本文简单分析插槽的执行过程,记录下关键时期的数据结构。
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
]
}
总结
插槽可以让调用组件时,写在标签内添加的内容显示出来。
<组件>
<template v-slot:插槽名="slot 的属性对象">
内容
</template>
</ 组件>
通过 resolveScopedSlots 返回对象 {插槽名: 内容函数},该对象作为创建组件 Vnode 的 data.scopedSlots 值。
在组件的 _render中会将
vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots,vm.$slots,vm.$scopedSlots)
组件内 <slot> 会编译成 renderSlot函数,内部会拿到上面内容函数,执行生成对应 Vnode。