Vue2 slot

282 阅读1分钟

本文简单分析插槽的执行过程,记录下关键时期的数据结构。

1、普通插槽执行过程

let AppLayout = {
  template: `
      <div class="container">
        <header><slot name="header"></slot></header>
        <main><slot>默认内容</slot></main>
        <footer><slot name="footer"></slot></footer>
      </div>
      `
}

let vm = new Vue({
  el: '#app',
  template: `
      <div>
        <app-layout>
          <template v-slot:header>
            <h1>{{title}}</h1>
          </template>

          <template v-slot:default>
            <p>{{msg}}</p>
          </template>

          <template v-slot:footer>
            <p>{{desc}}</p>
          </template>
        </app-layout>
      </div>
        `,
  data() {
    return {
      title: '我是标题',
      msg: '我是内容',
      desc: '其它信息'
    }
  },
  components: {
    AppLayout
  }
})
render
        _c('div',
            [
                _c('app-layout',
                    {
                        scopedSlots: _u(
                            [
                                {
                                    key: "header",
                                    fn: function () { return [_c('h1', [_v(_s(title))])] },
                                    proxy: true
                                },
                                {
                                    key: "default",
                                    fn: function () { return [_c('p', [_v(_s(msg))])] },
                                    proxy: true
                                }, {
                                    key: "footer",
                                    fn: function () { return [_c('p', [_v(_s(desc))])] },
                                    proxy: true
                                }
                            ])
                    }
                )
            ],
           1)

// _u = resolveScopedSlots

vnode
{
  tag:'div',
  context: Vue,
  children: [
    {
      tag: "vue-component-1-app-layout",
      componentOptions: {
        Ctor: VueComponent,
        children: undefined,
        listeners: undefined,
        propsData: undefined,
        tag: "app-layout",
      },
      data:{
        hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
        on: undefined,
        scopedSlots:{$stable: true, header: ƒ, default: ƒ, footer: ƒ}
      },
      context: Vue,
    }
  ]
}

组件 render
        _c('div',
            { staticClass: "container" },
            [
                _c('header', [_t("header")], 2),
                _v(" "),
                _c('main', [_t("default", function () { return [_v("默认内容")] })], 2),
                _v(" "),
                _c('footer', [_t("footer")], 2)
            ]
        )
// _t = renderSlot

_render
if (_parentVnode) {
  vm.$scopedSlots = normalizeScopedSlots(
    _parentVnode.data.scopedSlots,
    vm.$slots,
    vm.$scopedSlots
  )
}


生成 Vnode
{
  tag: 'div',
  parent: Vnode(vue-component-1-app-layout),
  data: {
    staticClass:'container'
  },
  context: VueComponent,
  children:[
    {tag: 'header',children:[h1Vnode]},
    {text: ' '},
    {tag: 'main',children:[default(3)Vnode]},
    {text: ' '},
    {tag: 'footer', children:[pVnode]}
  ]
}

slot 废弃用法转换流程,可忽略,这里备注下。

let AppLayout = {
  template: `
      <div class="container">
        <header><slot name="header"></slot></header>
        <main><slot>默认内容</slot></main>
        <footer><slot name="footer"></slot></footer>
      </div>
      `
}

let vm = new Vue({
  el: '#app',
  template: `
      <div>
        <app-layout>
        <h1 slot="header">{{title}}</h1>
        <p>{{msg}}</p>
        <p slot="footer">{{desc}}</p>
        </app-layout>
      </div>
        `,
  data() {
    return {
      title: '我是标题',
      msg: '我是内容',
      desc: '其它信息'
    }
  },
  components: {
    AppLayout
  }
})
_c(
  'div',
  [
    _c('app-layout',
       [
      _c('h1', { attrs: { "slot": "header" }, slot: "header" }, [_v(_s(title))]),
      _v(" "),
      _c('p', [_v(_s(msg))]),
      _v(" "),
      _c('p', { attrs: { "slot": "footer" }, slot: "footer" }, [_v(_s(desc))])
    ]
      )
  ],
  1)

生成 Vnode

{
  tag:'div',
  children: [
    {
      tag: "vue-component-1-app-layout",
      componentOptions: {
        Ctor: VueComponent,
        children: (5) [VNode, VNode, VNode, VNode, VNode], // h1, '', p, '', p
        listeners: undefined,
        propsData: undefined,
        tag: "app-layout",
      },
      data:{
        hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
        on: undefined
      },
      context: Vue,
    }
  ]
}

initInternalComponent:

opts._renderChildren = vnodeComponentOptions.children

initRender:

vm.$slots = resolveSlots(options._renderChildren, renderContext)

$slots
{
  default:(3) [VNode, VNode, VNode],
  footer:(1) [VNode],
  header:(1) [VNode], 
}

组件 render

_c(
  'div',
  { staticClass: "container" },
  [
    _c('header', [_t("header")], 2),
    _v(" "),
    _c('main', [_t("default", function () { return [_v("默认内容")] })], 2),
    _v(" "),
    _c('footer', [_t("footer")], 2)
  ]
)

// _t = renderSlot
// _v = createTextVNode

_render()

if (_parentVnode) {
  vm.$scopedSlots = normalizeScopedSlots(
    _parentVnode.data.scopedSlots,
    vm.$slots,
    vm.$scopedSlots
  )
}
vnode
{
  tag: 'div',
  parent: Vnode(vue-component-1-app-layout),
  data: {
    staticClass:'container'
  },
  context: VueComponent,
  children:[
    {tag: 'header',children:[h1Vnode]},
    {text: ' '},
    {tag: 'main',children:[default(3)Vnode]},
    {text: ' '},
    {tag: 'footer', children:[pVnode]}
  ]
}

接下来和正常生成 DOM 一样。

2、作用域插槽执行过程

let Child = {
  template: `
        <div class="child">
          <slot text="Hello " :msg="msg"></slot>
        </div>
        `,
  data() {
    return {
      msg: 'Vue'
    }
  }
}

let vm = new Vue({
  el: '#app',
  template: `
      <div>
        <child>
          <template v-slot="props">
            <p>Hello from parent</p>
            <p>{{ props.text + props.msg}}</p>
          </template>
        </child>
      </div>
        `,
  components: {
    Child
  }
})
render
        _c('div',
            [
                _c('child',
                    {
                        scopedSlots: _u([
                            {
                                key: "default",
                                fn: function (props) {
                                    return [_c('p', [_v("Hello from parent")]),
                                    _v(" "),
                                    _c('p', [_v(_s(props.text + props.msg))])]
                                }
                            }])
                    })
            ],
            1)

生成 Vnode
{
  tag:'div',
  context: Vue,
  children: [
    {
      tag: "vue-component-1-child",
      componentOptions: {
        Ctor: VueComponent,
        children: undefined,
        listeners: undefined,
        propsData: undefined,
        tag: "child",
      },
      data:{
        hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
        on: undefined,
        scopedSlots:{$stable: true, default: ƒ}
      },
      context: Vue,
    }
  ]
}

组件 render
 _c('div', { staticClass: "child" }, [
            _t("default", null, { "text": "Hello ", "msg": msg })
        ], 2)

_render 处理 vm.$scopedSlots

组件 Vnode
{
  tag: 'div',
  parent: Vnode(vue-component-1-child),
  data: {
    staticClass:'child'
  },
  context: VueComponent,
  children:[
    {tag: 'p',children:[Vnode]}, // Hello from parent
    {text: ' '},
    {tag: 'p',children:[Vnode]}, // Hello Vue
  ]
}

总结

image.png

插槽可以让调用组件时,写在标签内添加的内容显示出来。

<组件>
<template v-slot:插槽名="slot 的属性对象">
      内容
</template>
</ 组件>

通过 resolveScopedSlots 返回对象 {插槽名: 内容函数},该对象作为创建组件 Vnode 的 data.scopedSlots 值。

在组件的 _render中会将

vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots,vm.$slots,vm.$scopedSlots)

组件内 <slot> 会编译成 renderSlot函数,内部会拿到上面内容函数,执行生成对应 Vnode