废弃的 API 不在文章内容范围内。

基本用法

  1. <!-- 组件定义 -->
  2. <template>
  3. <div id="slot-component">
  4. <slot></slot>
  5. <slot name="hasNameSlot"></slot>
  6. <slot name="hasNameAndScopeSlot" :text="text"></slot>
  7. </div>
  8. </template>
  9. <!-- 使用插槽 -->
  10. <template>
  11. <slot-component>
  12. <span>我会去默认插槽</span>
  13. <tempalte #hasNameSlot>我会去第一个具名插槽</tempalte>
  14. <template #hasNameAndScopeSlot="{text}">{{text}}</template>
  15. </slot-component>
  16. </template>

原理解析

AST

插槽 与 其他正常的写法组件,最大的区别的起点实际是在 生成 AST 阶段 开始的,我们可以查看一下上述示例中 使用插槽 部分生成 AST 内容(忽略了没有意义的内容)。

主要特点 是,两个明确为具名插槽的节点,并不在 slot-componentchildren 里面,而是在其 scopedSlots 内以 key-value 的形式存储着。
原因 是解析器会在每个节点词法分析完后,会对其进行语法分析,其中有一个步骤就是对节点有可能有的插槽相关属性进行分析。具体执行栈大约是这样的:baseCompile -> parse -> parseHTML -> options.start -> options.end -> closeElement -> processSlotContent

  1. {
  2. "tag": "slot-component",
  3. "children": [
  4. {
  5. "type": 1,
  6. "tag": "div",
  7. "children": [
  8. {
  9. "type": 3,
  10. "text": "我会去默认插槽"
  11. }
  12. ],
  13. }
  14. ],
  15. "scopedSlots": {
  16. "\"hasNameSlot\"": {
  17. "tag": "template",
  18. "attrsMap": {
  19. "#hasNameSlot": ""
  20. },
  21. "children": [
  22. {
  23. "type": 1,
  24. "tag": "div",
  25. "children": [
  26. {
  27. "type": 3,
  28. "text": "我会去第一个具名插槽"
  29. }
  30. ],
  31. }
  32. ],
  33. "slotScope": "_empty_",
  34. "slotTarget": "\"hasNameSlot\"",
  35. "slotTargetDynamic": false
  36. },
  37. "\"hasNameAndScopeSlot\"": {
  38. "tag": "template",
  39. "attrsMap": {
  40. "#hasNameAndScopeSlot": "{ text }"
  41. },
  42. "children": [
  43. {
  44. "tag": "div",
  45. "children": [
  46. {
  47. "expression": "_s(text)",
  48. "tokens": [
  49. {
  50. "@binding": "text"
  51. }
  52. ],
  53. "text": "{{ text }}"
  54. }
  55. ],
  56. "plain": true
  57. }
  58. ],
  59. "slotScope": "{ text }",
  60. "slotTarget": "\"hasNameAndScopeSlot\"",
  61. "slotTargetDynamic": false
  62. }
  63. },
  64. }

Code generate

AST 生成后紧接着就是 code generate,生成是当前 AST 对应的 render 函数,让我们看看插槽部分和其他节点有什么不同。
从结构上来说,大体与 AST 保持一致,插槽的内容并没有到 children 部分,二是跑到了 节点属性 上,同时插槽内的内容变成的 函数式组件,相应的我们可以发现,作用域插槽就是通过调用这个 函数式组件,并 传入参数 而完成的。由于作用域的关系,函数内的变量会临时覆盖 this 上相同 key 的值,所以保证的语法的一致性。
具体的调用栈大约是这样的:baseCompiler -> generate -> genElement -> genData -> genScopedSlots

  1. with (this) {
  2. return _c(
  3. "slot-component",
  4. {
  5. scopedSlots: _u([
  6. {
  7. key: "hasnameslot",
  8. fn: function () {
  9. return [_c("div", [_v("我会去第一个具名插槽")])];
  10. },
  11. proxy: true,
  12. },
  13. {
  14. key: "hasnameandscopeslot",
  15. fn: function ({ text }) {
  16. return [_c("div", [_v(_s(text))])];
  17. },
  18. },
  19. ]),
  20. },
  21. [_c("div", [_v("我会去默认插槽")])]
  22. )
  23. }

Render

接下来一步是 Vue 是如何生成对应的虚拟 DOM 的呢,我们先需要去尝试解读一下生成的 render 函数里面的比较重要的函数 _c_u

_c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

https://github.com/vuejs/vue/blob/5255841aaff441d275122b4abfb099b881de7cb5/src/core/vdom/create-element.js#L28

  1. // 入参格式化
  2. export function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
  3. // 函数重载处理,如果 data 为数组或者基本数据类型,则视 data 实际缺省
  4. if (Array.isArray(data) || isPrimitive(data)) {
  5. normalizationType = children
  6. children = data
  7. data = undefined
  8. }
  9. // 开发者手动写的 render 函数才会为 true,此参数决定以什么模式格式化内容
  10. if (isTrue(alwaysNormalize)) {
  11. normalizationType = ALWAYS_NORMALIZE
  12. }
  13. return _createElement(context, tag, data, children, normalizationType)
  14. }
  15. export function _createElement ( context, tag, data, children, normalizationType) {
  16. // 如果 data(属性)为响应式的数据则抛出错误,返回空的虚拟 DOM
  17. if (isDef(data) && isDef((data: any).__ob__)) {
  18. process.env.NODE_ENV !== 'production' && warn(
  19. `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
  20. 'Always create fresh vnode data objects in each render!',
  21. context
  22. )
  23. return createEmptyVNode()
  24. }
  25. // component is 的写法处理
  26. if (isDef(data) && isDef(data.is)) {
  27. tag = data.is
  28. }
  29. // 如果 tag 为空则返回空的虚拟 DOM
  30. if (!tag) {
  31. return createEmptyVNode()
  32. }
  33. // 如果 key 值不是基本数据类型则抛出错误
  34. if (process.env.NODE_ENV !== 'production' &&
  35. isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  36. ) {
  37. if (!__WEEX__ || !('@binding' in data.key)) {
  38. warn(
  39. 'Avoid using non-primitive value as key, ' +
  40. 'use string/number value instead.',
  41. context
  42. )
  43. }
  44. }
  45. // 如果有 children 并且第一个是函数的话,则将第一个 child 转移到 scopedSlots.default 中,并清空 children
  46. if (Array.isArray(children) &&
  47. typeof children[0] === 'function'
  48. ) {
  49. data = data || {}
  50. data.scopedSlots = { default: children[0] }
  51. children.length = 0
  52. }
  53. if (normalizationType === ALWAYS_NORMALIZE) {
  54. children = normalizeChildren(children) // 复杂的规范化处理,因为 render 是开发者写的
  55. } else if (normalizationType === SIMPLE_NORMALIZE) {
  56. children = simpleNormalizeChildren(children) // 简单的规范化处理(拍平可能出现的嵌套数组 children)
  57. }
  58. let vnode, ns
  59. if (typeof tag === 'string') {
  60. // 标签为 string 时
  61. let Ctor
  62. ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  63. if (config.isReservedTag(tag)) {
  64. // 如果是原生标签
  65. // 则进行 v-on 的错误判断
  66. if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
  67. warn(
  68. `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
  69. context
  70. )
  71. }
  72. // 生成平台相对应的虚拟 DOM
  73. vnode = new VNode(
  74. config.parsePlatformTagName(tag), data, children,
  75. undefined, undefined, context
  76. )
  77. } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  78. // 如果不是原生标签同时相应的标签名称在 components 中定义了,则视为组件,并传入对应的构造函数,创建函数 VNode
  79. vnode = createComponent(Ctor, data, context, children, tag)
  80. } else {
  81. // 未知的标签,不管三七二十一直接用 tag 生成
  82. vnode = new VNode(
  83. tag, data, children,
  84. undefined, undefined, context
  85. )
  86. }
  87. } else {
  88. // 不是字符串则视为组件的构造函数等,直接创建函数 VNode
  89. vnode = createComponent(tag, data, context, children)
  90. }
  91. if (Array.isArray(vnode)) {
  92. return vnode
  93. } else if (isDef(vnode)) {
  94. if (isDef(ns)) applyNS(vnode, ns)
  95. if (isDef(data)) registerDeepBindings(data)
  96. return vnode
  97. } else {
  98. return createEmptyVNode()
  99. }
  100. }


_u = resolveScopedSlots

https://github.com/vuejs/vue/blob/5255841aaff441d275122b4abfb099b881de7cb5/src/core/instance/render-helpers/resolve-scoped-slots.js#L3

  1. // 将数值的 scopedSlots 转换成 key-value 的形式,同时加上一些渲染优化相关的属性
  2. // 参数内容由 codegen 时的 genScopedSlots 来决定
  3. // fns,就是插槽内容集合
  4. // res,正常都是初始值都为空,只不过递归处理的时候需要传递下去
  5. // hasDynamicKeys,通常情况下为 false(稳定的),如果相关节点有动态属性或者内容则为 true,这意味着需要在父节点更新的时候需要强制更新
  6. // contentHashKey,与第三个参数互斥,如果祖父组件有 v-if,则会有 key 值
  7. export function resolveScopedSlots (fns, res, hasDynamicKeys, contentHashKey) {
  8. // 初始化时根据 hasDynamicKeys 来判断是否是稳定的
  9. res = res || { $stable: !hasDynamicKeys }
  10. // 遍历 scopedSlots 里的内容
  11. for (let i = 0; i < fns.length; i++) {
  12. const slot = fns[i]
  13. if (Array.isArray(slot)) {
  14. // 如果还是数组则递归处理
  15. resolveScopedSlots(slot, res, hasDynamicKeys)
  16. } else if (slot) {
  17. // 如果是动态的则在渲染函数上也添加相关静态属性
  18. if (slot.proxy) {
  19. slot.fn.proxy = true
  20. }
  21. res[slot.key] = slot.fn
  22. }
  23. }
  24. // 有则加
  25. if (contentHashKey) {
  26. (res: any).$key = contentHashKey
  27. }
  28. return res
  29. }

VNode

基于上述讲解生成的虚拟DOM

  1. {
  2. tag: 'vue-component-1-slot-component',
  3. componentOptions: {
  4. Ctor: f VueComponent(options),
  5. children: [divVNode],
  6. tag: 'slot-component',
  7. listeners: undefined,
  8. propsData: undefined
  9. },
  10. context: {...VueInstance},
  11. data: {
  12. hook: {...VNodeHooks},
  13. on: undefined,
  14. scopedSlots: {
  15. $stable: true,
  16. hasnameandscopeslot: f({ text }),
  17. hasnameslot: f()
  18. }
  19. },
  20. ...otherKeys,
  21. }

组件如何处理父组件传下来的插槽内容

经过上面的讲解,我们可以直接跳过 AST,查看 render 函数

  1. with (this) {
  2. return _c(
  3. "div",
  4. { attrs: { id: "slot-component" } },
  5. [
  6. _t("default"),
  7. _v(" "),
  8. _t("hasNameSlot"),
  9. _v(" "),
  10. _t("hasNameAndScopeSlot", null, { text: text }),
  11. ],
  12. 2
  13. );
  14. }

很明显关键点就在 _t

_t = renderSlot

  1. export function renderSlot (name, fallback, props, bindObject) {
  2. // this 指向的就是上面生成的组件的实例,$scopedSlots 指向的就是我们上面折腾半天的内容
  3. // 先查看有没有传递下来相关的插槽
  4. const scopedSlotFn = this.$scopedSlots[name]
  5. let nodes
  6. if (scopedSlotFn) {
  7. // 如果有对应插槽
  8. // props 便是作用域内容
  9. props = props || {}
  10. // 如果绑定的是个对象则抛出错误并合并
  11. if (bindObject) {
  12. if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
  13. warn(
  14. 'slot v-bind without argument expects an Object',
  15. this
  16. )
  17. }
  18. props = extend(extend({}, bindObject), props)
  19. }
  20. // 调用渲染函数,传入参数,得到虚拟 VNode
  21. nodes = scopedSlotFn(props) || fallback
  22. } else {
  23. // 退而求其次去组件的插槽找
  24. nodes = this.$slots[name] || fallback
  25. }
  26. // 嵌套插槽处理
  27. const target = props && props.slot
  28. if (target) {
  29. return this.$createElement('template', { slot: target }, nodes)
  30. } else {
  31. return nodes
  32. }
  33. }

默认插槽是如何处理的

根据上面的分析,之后其实最大的问题就是,children 如何加入到 $scopeSlots 中,这个其实分两步走

  • 第一步:实例化 slot-component 组件阶段
  • 第二步:slot-component 生成 VNode 之前的准备工作时
    • Vue.proptotype._render 回先判断有没有父级节点,如果有则初始化 $scopeSlots
    • $scopeSlots 的初始化是通过 normalizeScopedSlots 函数将 _parentVnode.data.scopedSlotsthis.$slotthis.$scopedSlots(一般为空) 合并
    • 这个时候 this.$scopedSlots 就会有 default 指向父节点的 children,以及父组件的获得的具名插槽

在初始化完完成后,通过获取 this.$scopedSlots.default 就可以获取到默认插槽的内容啦!

参考