Skip to content

插槽实现原理

首先是父组件:

父组件使用

javascript
// 1. 父组件使用时
<ChildComponent class="component">
  <template v-slot:default="scope">
    {{ scope }}
  </template>
</ChildComponent>

父组件parse

javascript
// 2. 经过 parse 编译成 ast 后 (调用的是 processSlotContent 方法)
// 绑定的值(由子组件传递)
el.slotScope = scope
// 绑定的 slot 名称
el.slotTarget = "default"
// 动态绑定的 slot 名称
el.slotTargetDynamic = default
// template 内部 children 代码
el.children = []
// ChildComponent 对应的 ast 下的 scopedSlots 记录着 slot 的 ast
currentParent.scopedSlots = {}))[name] = element

父组件generate

genData

javascript
// 3. 经过 generate 生成代码后(先调用 genData 方法,此时会调用 genScopedSlots 方法)
if (el.slotTarget && !el.slotScope) {
  // 如果不存在 slotScope,表示没有传递参数
  data += `slot:${el.slotTarget},`
}
// 对于 ChildComponent 这个组件
if (el.scopedSlots) {
  data += `${genScopedSlots(el, el.scopedSlots, state)},`
}

genScopedSlots

javascript
// 4. genScopedSlots 方法
function genScopedSlots(
  el: ASTElement,
  slots: { [key: string]: ASTElement },
  state: CodegenState
): string {
  // 通过这里判断 slot 是否需要强制更新。
  let needsForceUpdate = el.for || Object.keys(slots).some(key => {
    const slot = slots[key]
    return (
      slot.slotTargetDynamic ||  // 如果存在动态绑定的类型
      slot.if || // 如果在 if 中
      slot.for || // 如果在 for 中
      containsSlotChild(slot) // 如果内部还存在 slot 标签
    )
  })

  let needsKey = !!el.if
  // 继续判断是否需要更新
  // 不需要强制更新时,判断是否在 for 循环或 if 当中
  // 判断 parent.slotScope 存在
  if (!needsForceUpdate) {
    let parent = el.parent
    while (parent) {
      if (
        (parent.slotScope && parent.slotScope !== emptySlotScopeToken) ||
        parent.for
      ) {
        needsForceUpdate = true
        break
      }
      if (parent.if) {
        needsKey = true
      }
      parent = parent.parent
    }
  }

  // 键值对形式: [{ key: "default": fn: fn, proxy: false/true }]
  const generatedSlots = Object.keys(slots)
    .map(key => genScopedSlot(slots[key], state))
    .join(',')

  return `scopedSlots:_u([${generatedSlots}]${needsForceUpdate ? `,null,true` : ``
    }${!needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : ``
    })`
}

genScopedSlot

javascript
function genScopedSlot(
  el: ASTElement,
  state: CodegenState
): string {
  // 1. 使用到的变量,将其作为参数
  const slotScope = el.slotScope === emptySlotScopeToken
    ? ``
    : String(el.slotScope)
  // 2. 定义可以生成 vnode 的函数,并传入定义的参数
  const fn = `function(${slotScope}){` +
    `return ${el.tag === 'template'
      ? el.if && isLegacySyntax
        ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
        : genChildren(el, state) || 'undefined'
      : genElement(el, state)
    }}`
  const reverseProxy = slotScope ? `` : `,proxy:true`
  // 3. 生成键值对形式
  return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
}

父组件render

javascript
export function resolveScopedSlots(
  /**
   * 例如
   * [
   *  {
   *    key: default
   *    fn: (user) => {...}
   *  }
   * ]
   */
  fns: ScopedSlotsData,
  res?: Object,  // null 或者 undefined 或 {xx}
  hasDynamicKeys?: boolean, // 是否强制更新
  contentHashKey?: number // 不强制更新时的 hash
): { [key: string]: Function, $stable: boolean } {
  // ! $stable 根据 hasDynamicKeys 判断,在 generate 的时候已经生成
  res = res || { $stable: !hasDynamicKeys }
  for (let i = 0;i < fns.length;i++) {
    const slot = fns[i]
    if (Array.isArray(slot)) {
      resolveScopedSlots(slot, res, hasDynamicKeys)
    } else if (slot) {
      if (slot.proxy) {
        slot.fn.proxy = true
      }
      res[slot.key] = slot.fn
    }
  }
  // 转换成了 res { default: fn1, header: fn2  }
  if (contentHashKey) {
    (res: any).$key = contentHashKey
  }
  return res
}

子组件开始实例化

父组件编译完毕后,更新时,会遇到ChildComponent组件,进入到子组件的创建环节,此时data里面包含处理后的scopedSlots

javascript
export function createComponentInstanceForVnode (
  vnode: any,
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode, // vnode.data 中存放着 scopedSlots
    parent // 指向当前更新的组件的实例
  }

  // render 阶段 createComponent 生成vnode的时候,
  // options 都存在componentOptions中
  return new vnode.componentOptions.Ctor(options)
}

子组件合并选项

javascript
export function initInternalComponent(vm: Component, options: InternalComponentOptions) {
  // * Vue 构造函数的 options
  // * const options: InternalComponentOptions = {
  // *   _isComponent: true,
  // *   _parentVnode: vnode,
  // *   parent
  // * }
  const opts = vm.$options = Object.create(vm.constructor.options)
  // _parentVnode 为组件的 vnode
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  // 组件传值
  opts.propsData = vnodeComponentOptions.propsData
  // 组件监听的事件
  opts._parentListeners = vnodeComponentOptions.listeners
  // 组件内部的内容解析为 children。
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag
}

子组件数据初始化

javascript
export function initRender(vm: Component) {
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode
  const renderContext = parentVnode && parentVnode.context
  // _renderChildren 为父组件调用子组件时子组件之间的节点。
  // 通过 resolveSlots方法 将 children 转换成 slots 形式
  // slots = { default: [Vnode1, Vnode2], [name]: [Vnode], ... }
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
}

export function resolveSlots(
  children: ?Array<VNode>,
  context: ?Component
): { [key: string]: Array<VNode> } {
  if (!children || !children.length) {
    return {}
  }
  const slots = {}
  for (let i = 0, l = children.length;i < l;i++) {
    const child = children[i]
    const data = child.data
    // 删除 data.attrs.slot 属性
    if (data && data.attrs && data.attrs.slot) {
      delete data.attrs.slot
    }
    // 如果 data.slot 存在,说明此时 slotScope 不存在(genData时处理的)
    if ((child.context === context || child.fnContext === context) &&
      data && data.slot != null
    ) {
      const name = data.slot
      const slot = (slots[name] || (slots[name] = []))
      if (child.tag === 'template') {
        slot.push.apply(slot, child.children || [])
      } else {
        slot.push(child)
      }
    } else {
      // 其余的默认放到 default 中
      (slots.default || (slots.default = [])).push(child)
    }
  }
  // 忽略无用 slot
  for (const name in slots) {
    if (slots[name].every(isWhitespace)) {
      delete slots[name]
    }
  }
  return slots
}

子组件slot使用

javascript
<slot name="header"></slot>

子组件slot parse

javascript
function processSlotOutlet (el) {
  if (el.tag === 'slot') {
    // 添加 slotName 属性
    el.slotName = getBindingAttr(el, 'name');
  }
}

子组件slot generate

javascript
function genSlot(el: ASTElement, state: CodegenState): string {
  const slotName = el.slotName || '"default"'
  // 默认的 children 节点
  const children = genChildren(el, state)
  // _t 函数 
  let res = `_t(${slotName}${children ? `,${children}` : ''}`
  // 静态属性 + 动态绑定属性
  const attrs = el.attrs || el.dynamicAttrs
    ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
      name: camelize(attr.name),
      value: attr.value,
      dynamic: attr.dynamic
    })))
    : null
  const bind = el.attrsMap['v-bind']
  if ((attrs || bind) && !children) {
    res += `,null`
  }
  if (attrs) {
    res += `,${attrs}`
  }
  if (bind) {
    res += `${attrs ? '' : ',null'},${bind}`
  }
  return res + ')'
}

子组件 render

_render

javascript
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      // 如果有插槽有 scope 的时候,scopedSlots存在
      _parentVnode.data.scopedSlots,
      // initRender 解析出来的 slots,通过 children 转换而来
      vm.$slots,
      // 当前的 scopedSlots
      vm.$scopedSlots
    )
  }
  // ...
}

normalizeScopedSlots

javascript
export function normalizeScopedSlots(
  // 有 scope 时解析出来的 scopedSlots
  slots: { [key: string]: Function } | void,
  // children 转换而来的 slots,都是没有 slotScope 的 slot
  normalSlots: { [key: string]: Array<VNode> },
  // 当前组件的原来的 scopeSlots
  prevSlots?: { [key: string]: Function } | void
): any {
  let res
  // 是否具有普通的 slots
  const hasNormalSlots = Object.keys(normalSlots).length > 0
  // $stable 表示是否需要强制更新,不需要则为true
  const isStable = slots ? !!slots.$stable : !hasNormalSlots
  const key = slots && slots.$key
  if (!slots) {
   // ...
  } else {
    res = {}
    for (const key in slots) {
      if (slots[key] && key[0] !== '$') {
        // 用 scope 的覆盖 没有 scope 的
        res[key] = normalizeScopedSlot(normalSlots, key, slots[key])
      }
    }
  }
 
  // 将普通的内容也转换成 scopedSlots 形式
  for (const key in normalSlots) {
    if (!(key in res)) {
      // 将 normal 转变为函数形式
      res[key] = proxyNormalSlot(normalSlots, key)
    }
  }

  // * 当前的插槽上面设置的属性
  def(res, '$stable', isStable)
  def(res, '$key', key)
  def(res, '$hasNormal', hasNormalSlots)
  // res 里包含了
  return res
}

renderSlot

javascript
export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  // 获取对应函数
  const scopedSlotFn = this.$scopedSlots[name]
  let nodes
  // 如果存在对应的函数
  if (scopedSlotFn) { // scoped slot
    props = props || {}
    if (bindObject) {
      // 处理参数
      props = extend(extend({}, bindObject), props)
    }
    // 执行函数,返回 vnodes
    nodes = scopedSlotFn(props) || fallback
  } else {
    // 如果找不到对应的 name,那么使用默认的 vnodes
    nodes = this.$slots[name] || fallback
  }

  const target = props && props.slot
  if (target) {
    return this.$createElement('template', { slot: target }, nodes)
  } else {
    return nodes
  }
}

总结

插槽在处理的时候主要分为两种类型:

  • 一种是带有slotScope的,此时具有参数传入。
  • 一种是不带有slotScope,为普通插槽。

在编译的时候:

  • 带有slotScope的转换为scopedSlots对象,存放到component组件的vnode.data里面:键为slot name,值为能生成vnode的函数(函数的参数与scope值一致)。
  • 不带有slotScope的标记但是slotTarget存在,此时会标记el.data.slottrue
  • 对于组件下的内容,全部都会存放到vnode.componentOptions.children属性中。

在子组件初始化的时候:

  • vm.$options._renderChildren表示children
  • children内容中存在data.slotchild,转化到vm.$slots属性当中,为{ default: [Vnode1, Vnodee2] }形式。

在子组件渲染的时候:

  • vm.$slotsvm.$scopedSlots合并到vm.$scopedSlots,转换为{ default: (props) => Vnodes}的形式。
  • 渲染slot组件时,根据namevm.$scopedSlots中获取对应的函数,执行返回vnodes