Vue 的组件提供了一个非常有用的特性 —— slot 插槽,它让组件的实现变的更加灵活。平时在开发组件库的时候,为了让组件更加灵活可定制,经常用插槽的方式让用户可以自定义内容。插槽分为普通插槽和作用域插槽,它们可以解决不同的场景

普通插槽

例子

  1. let AppLayout = {
  2. template: '<div class="container">' +
  3. '<header><slot name="header"></slot></header>' +
  4. '<main><slot>默认内容</slot></main>' +
  5. '<footer><slot name="footer"></slot></footer>' +
  6. '</div>'
  7. }
  8. let vm = new Vue({
  9. el: '#app',
  10. template: '<div>' +
  11. '<app-layout>' +
  12. '<h1 slot="header">{{title}}</h1>' +
  13. '<p>{{msg}}</p>' +
  14. '<p slot="footer">{{desc}}</p>' +
  15. '</app-layout>' +
  16. '</div>',
  17. data() {
  18. return {
  19. title: '我是标题',
  20. msg: '我是内容',
  21. desc: '其它信息'
  22. }
  23. },
  24. components: {
  25. AppLayout
  26. }
  27. })

定义了 AppLayout 子组件,它内部定义了 3 个插槽,2 个为具名插槽,一个 name 为 header,一个 name 为 footer,还有一个没有定义 name 的是默认插槽
之前填写的内容为默认内容
父组件注册和引用了 AppLayout 的组件,并在组件内部定义了一些元素,用来替换插槽,那么它最终生成的 DOM 如下

  1. <div>
  2. <div class="container">
  3. <header><h1>我是标题</h1></header>
  4. <main><p>我是内容</p></main>
  5. <footer><p>其它信息</p></footer>
  6. </div>
  7. </div>

编译

编译是发生在调用 vm.$mount 的时候,所以编译的顺序是先编译父组件,再编译子组件
首先编译父组件,在 parse 阶段,会执行 processSlot 处理 slot
定义在 src/compiler/parser/index.js 中

  1. // handle content being passed to a component as slot,
  2. // e.g. <template slot="xxx">, <div slot-scope="xxx">
  3. function processSlotContent (el) {
  4. let slotScope
  5. // <template slot="xxx">
  6. if (el.tag === 'template') {
  7. slotScope = getAndRemoveAttr(el, 'scope')
  8. /* istanbul ignore if */
  9. if (process.env.NODE_ENV !== 'production' && slotScope) {
  10. warn(
  11. `the "scope" attribute for scoped slots have been deprecated and ` +
  12. `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
  13. `can also be used on plain elements in addition to <template> to ` +
  14. `denote scoped slots.`,
  15. el.rawAttrsMap['scope'],
  16. true
  17. )
  18. }
  19. el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
  20. } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) { // <div slot-scope="xxx">
  21. /* istanbul ignore if */
  22. if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
  23. warn(
  24. `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
  25. `(v-for takes higher priority). Use a wrapper <template> for the ` +
  26. `scoped slot to make it clearer.`,
  27. el.rawAttrsMap['slot-scope'],
  28. true
  29. )
  30. }
  31. el.slotScope = slotScope
  32. }
  33. // slot="xxx"
  34. // 当解析到标签上有slot属性的时候,会给对应的AST元素节点添加slotTarget属性
  35. const slotTarget = getBindingAttr(el, 'slot')
  36. if (slotTarget) {
  37. el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
  38. el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
  39. // preserve slot as an attribute for native shadow DOM compat
  40. // only for non-scoped slots.
  41. if (el.tag !== 'template' && !el.slotScope) {
  42. addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
  43. }
  44. }
  45. // 2.6 v-slot syntax
  46. if (process.env.NEW_SLOT_SYNTAX) {
  47. if (el.tag === 'template') {
  48. // v-slot on <template>
  49. const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
  50. if (slotBinding) {
  51. if (process.env.NODE_ENV !== 'production') {
  52. if (el.slotTarget || el.slotScope) {
  53. warn(
  54. `Unexpected mixed usage of different slot syntaxes.`,
  55. el
  56. )
  57. }
  58. if (el.parent && !maybeComponent(el.parent)) {
  59. warn(
  60. `<template v-slot> can only appear at the root level inside ` +
  61. `the receiving component`,
  62. el
  63. )
  64. }
  65. }
  66. const { name, dynamic } = getSlotName(slotBinding)
  67. el.slotTarget = name
  68. el.slotTargetDynamic = dynamic
  69. el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
  70. }
  71. } else {
  72. // v-slot on component, denotes default slot
  73. const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
  74. if (slotBinding) {
  75. if (process.env.NODE_ENV !== 'production') {
  76. if (!maybeComponent(el)) {
  77. warn(
  78. `v-slot can only be used on components or <template>.`,
  79. slotBinding
  80. )
  81. }
  82. if (el.slotScope || el.slotTarget) {
  83. warn(
  84. `Unexpected mixed usage of different slot syntaxes.`,
  85. el
  86. )
  87. }
  88. if (el.scopedSlots) {
  89. warn(
  90. `To avoid scope ambiguity, the default slot should also use ` +
  91. `<template> syntax when there are other named slots.`,
  92. slotBinding
  93. )
  94. }
  95. }
  96. // add the component's children to its default slot
  97. const slots = el.scopedSlots || (el.scopedSlots = {})
  98. const { name, dynamic } = getSlotName(slotBinding)
  99. const slotContainer = slots[name] = createASTElement('template', [], el)
  100. slotContainer.slotTarget = name
  101. slotContainer.slotTargetDynamic = dynamic
  102. slotContainer.children = el.children.filter((c: any) => {
  103. if (!c.slotScope) {
  104. c.parent = slotContainer
  105. return true
  106. }
  107. })
  108. slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
  109. // remove children as they are returned from scopedSlots now
  110. el.children = []
  111. // mark el non-plain so data gets generated
  112. el.plain = false
  113. }
  114. }
  115. }
  116. }

在codegen阶段genData中会处理slotTarget,相关代码在src/compiler/codegen/index.js中

  1. // slot target
  2. // only for non-scoped slotTarget
  3. // 给data添加一个slot属性,并指向slot
  4. if (el.slotTarget && !el.slotScope) {
  5. data += `slot:${el.slotTarget},`
  6. }
  7. // scoped slots
  8. if (el.scopedSlots) {
  9. data += `${genScopedSlots(el, el.scopedSlots, state)},`
  10. }

例子父组件最终生成代码如下

  1. with(this){
  2. return _c('div',
  3. [
  4. _c('app-layout',
  5. [
  6. _c('h1',{attrs:{"slot":"header"},slot:"header"},[_v(_s(title))]),
  7. _c('p',[_v(_s(msg))]),
  8. _c('p',{attrs:{"slot":"footer"},slot:"footer"},[_v(_s(desc))])
  9. ]
  10. )
  11. ],
  12. 1)
  13. }

编译子组件,同样在parser阶段会执行processSlot处理函数
定义在src/compiler/codegen/index.js中

  1. function genSlot (el: ASTElement, state: CodegenState): string {
  2. // 从AST元素节点对应的属性上取 默认是 default
  3. const slotName = el.slotName || '"default"'
  4. // children对应的就是slot开始和闭合标签包裹的内容
  5. const children = genChildren(el, state)
  6. let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  7. const attrs = el.attrs || el.dynamicAttrs
  8. ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
  9. // slot props are camelized
  10. name: camelize(attr.name),
  11. value: attr.value,
  12. dynamic: attr.dynamic
  13. })))
  14. : null
  15. const bind = el.attrsMap['v-bind']
  16. if ((attrs || bind) && !children) {
  17. res += `,null`
  18. }
  19. if (attrs) {
  20. res += `,${attrs}`
  21. }
  22. if (bind) {
  23. res += `${attrs ? '' : ',null'},${bind}`
  24. }
  25. return res + ')'
  26. }

最终子组件生成的代码如下

  1. with(this) {
  2. return _c('div',{
  3. staticClass:"container"
  4. },[
  5. _c('header',[_t("header")],2),
  6. _c('main',[_t("default",[_v("默认内容")])],2),
  7. _c('footer',[_t("footer")],2)
  8. ]
  9. )
  10. }

在编译章节了解到,_t函数对应的就是renderSlot方法,它的定义在src/core/instance/render-heplpers/render-slot.js中

  1. /**
  2. * Runtime helper for rendering <slot>
  3. */
  4. export function renderSlot (
  5. name: string, // 插槽名称slotName
  6. fallbackRender: ?((() => Array<VNode>) | Array<VNode>), // 插槽的默认内容生成的vnode数组
  7. props: ?Object,
  8. bindObject: ?Object
  9. ): ?Array<VNode> {
  10. const scopedSlotFn = this.$scopedSlots[name]
  11. let nodes
  12. if (scopedSlotFn) {
  13. // scoped slot
  14. props = props || {}
  15. if (bindObject) {
  16. if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
  17. warn('slot v-bind without argument expects an Object', this)
  18. }
  19. props = extend(extend({}, bindObject), props)
  20. }
  21. nodes =
  22. scopedSlotFn(props) ||
  23. (typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)
  24. } else {
  25. // 如果this.$slot[name]有值,就返回它对应的vnode数组,否则返回fallbackRender
  26. nodes =
  27. this.$slots[name] ||
  28. (typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)
  29. }
  30. const target = props && props.slot
  31. if (target) {
  32. return this.$createElement('template', { slot: target }, nodes)
  33. } else {
  34. return nodes
  35. }
  36. }

子组件的 init 时机是在父组件执行 patch 过程的时候,那这个时候父组件已经编译完成了。并且子组件在 init 过程中会执行 initRender 函数,initRender 的时候获取到 vm.$slot,相关代码在 src/core/instance/render.js 中

  1. export function initRender (vm: Component) {
  2. // ...
  3. const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  4. const renderContext = parentVnode && parentVnode.context
  5. vm.$slots = resolveSlots(options._renderChildren, renderContext)
  6. // ...
  7. }

resolveSlots定义在src/core/instance/render-helpers/resolve-slots.js中

  1. /**
  2. * Runtime helper for resolving raw children VNodes into a slot object.
  3. */
  4. export function resolveSlots (
  5. children: ?Array<VNode>, // 对应的是父vnode的children
  6. context: ?Component // 父vnode的上下文,也就是父组件的vm实例
  7. ): { [key: string]: Array<VNode> } {
  8. if (!children || !children.length) {
  9. return {}
  10. }
  11. const slots = {}
  12. // 遍历
  13. for (let i = 0, l = children.length; i < l; i++) {
  14. const child = children[i]
  15. // data
  16. const data = child.data
  17. // remove slot attribute if the node is resolved as a Vue slot node
  18. if (data && data.attrs && data.attrs.slot) {
  19. delete data.attrs.slot
  20. }
  21. // named slots should only be respected if the vnode was rendered in the
  22. // same context.
  23. if ((child.context === context || child.fnContext === context) &&
  24. data && data.slot != null
  25. ) {
  26. // 插槽名称 编译父组件在codegen阶段设置的data.slot
  27. const name = data.slot
  28. // 以name为key把child添加到slots中
  29. const slot = (slots[name] || (slots[name] = []))
  30. if (child.tag === 'template') {
  31. slot.push.apply(slot, child.children || [])
  32. } else {
  33. slot.push(child)
  34. }
  35. } else { // data.slot不存在则是默认插槽的内容
  36. (slots.default || (slots.default = [])).push(child)
  37. }
  38. }
  39. // ignore slots that contains only whitespace
  40. for (const name in slots) {
  41. if (slots[name].every(isWhitespace)) {
  42. delete slots[name]
  43. }
  44. }
  45. return slots
  46. }

获取到整个slots,它是一个对象,key是插槽名称,value是一个vnode类型的数组,因为它可以有多个同名插槽
拿到了 vm.$slots 了,回到 renderSlot 函数

  1. nodes = this.$slots[name] || (typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)

就能根据插槽名称获取到对应的 vnode 数组了,这个数组里的 vnode 都是在父组件创建的,这样就实现了在父组件替换子组件插槽的内容了
在普通插槽中,父组件应用到子组件插槽里的数据都是绑定到父组件的,因为它渲染成 vnode 的时机的上下文是父组件的实例

作用域插槽

例子

  1. let Child = {
  2. template: '<div class="child">' +
  3. '<slot text="Hello" :msg="msg"></slot>' +
  4. '</div>',
  5. data() {
  6. return {
  7. msg: 'Vue'
  8. }
  9. }
  10. }
  11. let vm = new Vue({
  12. el: '#app',
  13. template: '<div>' +
  14. '<child>' +
  15. '<template slot-scope="props">' +
  16. '<p>Hello from parent</p>' +
  17. '<p>{{ props.text + props.msg}}</p>' +
  18. '</template>' +
  19. '</child>' +
  20. '</div>',
  21. components: {
  22. Child
  23. }
  24. })

最终生成的 DOM 结构如下

  1. <div>
  2. <div class="child">
  3. <p>Hello from parent</p>
  4. <p>Hello Vue</p>
  5. </div>
  6. </div>

父组件实现插槽的部分多了一个 template 标签,以及 scope-slot 属性,其实在 Vue 2.5+ 版本,scoped-slot 可以作用在普通元素上
这些就是作用域插槽和普通插槽在写法上的差别

编译

在编译阶段,仍然是先编译父组件,同样是通过processSlotContent函数去处理 scoped-slot
定义在在 src/compiler/parser/index.js 中

  1. // handle content being passed to a component as slot,
  2. // e.g. <template slot="xxx">, <div slot-scope="xxx">
  3. function processSlotContent (el) {
  4. let slotScope
  5. if (el.tag === 'template') { // 读取scoped-slot属性并赋值给当前AST元素节点的slotName属性
  6. slotScope = getAndRemoveAttr(el, 'scope')
  7. /* istanbul ignore if */
  8. if (process.env.NODE_ENV !== 'production' && slotScope) {
  9. warn(
  10. `the "scope" attribute for scoped slots have been deprecated and ` +
  11. `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
  12. `can also be used on plain elements in addition to <template> to ` +
  13. `denote scoped slots.`,
  14. el.rawAttrsMap['scope'],
  15. true
  16. )
  17. }
  18. el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
  19. } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
  20. /* istanbul ignore if */
  21. if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
  22. warn(
  23. `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
  24. `(v-for takes higher priority). Use a wrapper <template> for the ` +
  25. `scoped slot to make it clearer.`,
  26. el.rawAttrsMap['slot-scope'],
  27. true
  28. )
  29. }
  30. el.slotScope = slotScope
  31. }
  32. // ...
  33. }

接下来在构造 AST 树的时候,会执行以下逻辑

  1. if (element.elseif || element.else) {
  2. processIfConditions(element, currentParent)
  3. } else {
  4. if (element.slotScope) { // 对于拥有 scopedSlot 属性的 AST 元素节点而言,是不会作为 children 添加到当前 AST 树中,而是存到父 AST 元素节点的 scopedSlots 属性上,它是一个对象,以插槽名称 name 为 key
  5. // scoped slot
  6. // keep it in the children list so that v-else(-if) conditions can
  7. // find it as the prev node.
  8. const name = element.slotTarget || '"default"'
  9. ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  10. }
  11. currentParent.children.push(element)
  12. element.parent = currentParent
  13. }

然后在 genData 的过程,会对 scopedSlots 做处理

  1. // scoped slots
  2. if (el.scopedSlots) {
  3. data += `${genScopedSlots(el, el.scopedSlots, state)},`
  4. }
  5. function genScopedSlots (
  6. el: ASTElement,
  7. slots: { [key: string]: ASTElement },
  8. state: CodegenState
  9. ): string {
  10. // by default scoped slots are considered "stable", this allows child
  11. // components with only scoped slots to skip forced updates from parent.
  12. // but in some cases we have to bail-out of this optimization
  13. // for example if the slot contains dynamic names, has v-if or v-for on them...
  14. let needsForceUpdate = el.for || Object.keys(slots).some(key => {
  15. const slot = slots[key]
  16. return (
  17. slot.slotTargetDynamic ||
  18. slot.if ||
  19. slot.for ||
  20. containsSlotChild(slot) // is passing down slot from parent which may be dynamic
  21. )
  22. })
  23. // #9534: if a component with scoped slots is inside a conditional branch,
  24. // it's possible for the same component to be reused but with different
  25. // compiled slot content. To avoid that, we generate a unique key based on
  26. // the generated code of all the slot contents.
  27. let needsKey = !!el.if
  28. // OR when it is inside another scoped slot or v-for (the reactivity may be
  29. // disconnected due to the intermediate scope variable)
  30. // #9438, #9506
  31. // TODO: this can be further optimized by properly analyzing in-scope bindings
  32. // and skip force updating ones that do not actually use scope variables.
  33. if (!needsForceUpdate) {
  34. let parent = el.parent
  35. while (parent) {
  36. if (
  37. (parent.slotScope && parent.slotScope !== emptySlotScopeToken) ||
  38. parent.for
  39. ) {
  40. needsForceUpdate = true
  41. break
  42. }
  43. if (parent.if) {
  44. needsKey = true
  45. }
  46. parent = parent.parent
  47. }
  48. }
  49. // 对scopedSlots对象遍历执行genScopedSlots,并把结果用逗号拼接
  50. const generatedSlots = Object.keys(slots)
  51. .map(key => genScopedSlot(slots[key], state))
  52. .join(',')
  53. return `scopedSlots:_u([${generatedSlots}]${
  54. needsForceUpdate ? `,null,true` : ``
  55. }${
  56. !needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : ``
  57. })`
  58. }
  59. function genScopedSlot (
  60. el: ASTElement,
  61. state: CodegenState
  62. ): string {
  63. const isLegacySyntax = el.attrsMap['slot-scope']
  64. if (el.if && !el.ifProcessed && !isLegacySyntax) {
  65. return genIf(el, state, genScopedSlot, `null`)
  66. }
  67. if (el.for && !el.forProcessed) {
  68. return genFor(el, state, genScopedSlot)
  69. }
  70. const slotScope = el.slotScope === emptySlotScopeToken
  71. ? ``
  72. : String(el.slotScope)
  73. // 先生成一段函数代码,参数是slotScope(标签属性上的scoped-slot对应的值)
  74. const fn = `function(${slotScope}){` +
  75. `return ${el.tag === 'template'
  76. ? el.if && isLegacySyntax
  77. ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
  78. : genChildren(el, state) || 'undefined'
  79. : genElement(el, state)
  80. }}`
  81. // reverse proxy v-slot without scope on this.$slots
  82. const reverseProxy = slotScope ? `` : `,proxy:true`
  83. // 返回一个对象 key为插槽名称 fn为生成的函数代码
  84. return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
  85. }

对于例子而言,父组件最终生成的代码如下

  1. with(this){
  2. return _c('div',
  3. [_c('child',
  4. {scopedSlots:_u([
  5. {
  6. key: "default",
  7. fn: function(props) {
  8. return [
  9. _c('p',[_v("Hello from parent")]),
  10. _c('p',[_v(_s(props.text + props.msg))])
  11. ]
  12. }
  13. }])
  14. }
  15. )],
  16. 1)
  17. }

可以看到它和普通插槽父组件编译结果的一个很明显的区别就是没有 children 了,data 部分多了一个对象,并且执行了 _u 方法
_u 函数对的就是 resolveScopedSlots 方法
定义在 src/core/instance/render-heplpers/resolve-scoped-slots.js 中

  1. export function resolveScopedSlots (
  2. fns: ScopedSlotsData, // see flow/vnode 数组 每一个元素都有一个key和一个fn key对应插槽名称 fn对应一个函数
  3. res?: Object,
  4. // the following are added in 2.6
  5. hasDynamicKeys?: boolean,
  6. contentHashKey?: number
  7. ): { [key: string]: Function, $stable: boolean } {
  8. res = res || { $stable: !hasDynamicKeys }
  9. // 遍历这个fns数组,生成一个对象,对象的key就是插槽名称,value就是函数
  10. for (let i = 0; i < fns.length; i++) {
  11. const slot = fns[i]
  12. if (Array.isArray(slot)) {
  13. resolveScopedSlots(slot, res, hasDynamicKeys)
  14. } else if (slot) {
  15. // marker for reverse proxying v-slot without scope on this.$slots
  16. if (slot.proxy) {
  17. slot.fn.proxy = true
  18. }
  19. res[slot.key] = slot.fn
  20. }
  21. }
  22. if (contentHashKey) {
  23. (res: any).$key = contentHashKey
  24. }
  25. return res
  26. }

子组件的编译,和普通插槽的过程基本相同,唯一一点区别是在 genSlot 的时候

  1. function genSlot (el: ASTElement, state: CodegenState): string {
  2. const slotName = el.slotName || '"default"'
  3. const children = genChildren(el, state)
  4. let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  5. const attrs = el.attrs || el.dynamicAttrs
  6. ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
  7. // slot props are camelized
  8. name: camelize(attr.name),
  9. value: attr.value,
  10. dynamic: attr.dynamic
  11. })))
  12. : null
  13. const bind = el.attrsMap['v-bind']
  14. if ((attrs || bind) && !children) {
  15. res += `,null`
  16. }
  17. if (attrs) {
  18. res += `,${attrs}`
  19. }
  20. if (bind) {
  21. res += `${attrs ? '' : ',null'},${bind}`
  22. }
  23. return res + ')'
  24. }

它会对 attrs 和 v-bind 做处理,对应例子,最终生成的代码如下

  1. with(this){
  2. return _c('div',
  3. {staticClass:"child"},
  4. [_t("default",null,
  5. {text:"Hello ",msg:msg}
  6. )],
  7. 2)
  8. }

_t 方法对应的是 renderSlot 方法

  1. /**
  2. * Runtime helper for rendering <slot>
  3. */
  4. export function renderSlot (
  5. name: string,
  6. fallbackRender: ?((() => Array<VNode>) | Array<VNode>),
  7. props: ?Object,
  8. bindObject: ?Object
  9. ): ?Array<VNode> {
  10. const scopedSlotFn = this.$scopedSlots[name]
  11. let nodes
  12. if (scopedSlotFn) {
  13. // scoped slot
  14. props = props || {}
  15. if (bindObject) {
  16. if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
  17. warn('slot v-bind without argument expects an Object', this)
  18. }
  19. props = extend(extend({}, bindObject), props)
  20. }
  21. nodes =
  22. scopedSlotFn(props) ||
  23. (typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)
  24. } else {
  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. }

这个 this.$scopedSlots 又是在什么地方定义的呢,原来在子组件的渲染函数执行前,在 vm_render 方法内,有这么一段逻辑,定义在 src/core/instance/render.js 中

  1. if (_parentVnode) {
  2. vm.$scopedSlots = normalizeScopedSlots(
  3. _parentVnode.data.scopedSlots,
  4. vm.$slots,
  5. vm.$scopedSlots
  6. )
  7. }

这个 _parentVNode.data.scopedSlots 对应的就是父组件通过执行 resolveScopedSlots 返回的对象
所以回到 genSlot 函数,通过插槽的名称拿到对应的 scopedSlotFn,然后把相关的数据扩展到 props 上,作为函数的参数传入,函数这个时候执行,然后返回生成的 vnodes,为后续渲染节点用

了解普通插槽和作用域插槽的实现
它们有一个很大的差别是数据作用域,普通插槽是在父组件编译和渲染阶段生成 vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes。而对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例
两种插槽的目的都是让子组件 slot 占位符生成的内容由父组件来决定,但数据的作用域会根据它们 vnodes 渲染时机不同而不同