插槽编译原理
📖 本节总结
Vue 模板中的插槽在编译阶段被转换为 children 或 $slots,运行时通过 renderSlot 函数渲染。
插槽的编译过程
父组件模板
vue
<!-- Parent.vue -->
<template>
<Child>
<template #header>
<h1>标题</h1>
</template>
<template #default>
<p>默认内容</p>
</template>
<template #footer="{ userName }">
<span>作者: {{ userName }}</span>
</template>
</Child>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
编译后
javascript
// Parent.vue 的 render 函数
function render(_ctx, _cache) {
return h(Child, null, {
header: () => [h('h1', '标题')],
default: () => [h('p', '默认内容')],
footer: ({ userName }) => [h('span', `作者: ${userName}`)]
})
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
子组件模板
vue
<!-- Child.vue -->
<template>
<div class="card">
<slot name="header"></slot>
<slot></slot>
<slot name="footer" :userName="author"></slot>
</div>
</template>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
编译后
javascript
// Child.vue 的 render 函数
function render(_ctx, _cache) {
return h('div', { class: 'card' }, [
// 插槽渲染
renderSlot(_ctx.$slots, 'header'),
renderSlot(_ctx.$slots, 'default'),
renderSlot(_ctx.$slots, 'footer', { userName: _ctx.author })
])
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
renderSlot 函数
javascript
// packages/runtime-core/src/helpers/renderSlot.ts
export function renderSlot(
slots, // $slots 对象
name, // 插槽名称
props, // 传递给插槽的 props(作用域插槽)
fallback // 默认内容
) {
// 获取对应名称的插槽
let slot = slots[name]
// 如果插槽不存在,使用 fallback
if (!slot) {
return fallback ? fallback() : null
}
// 执行插槽函数,传入 props
// 这样作用域插槽可以访问子组件的数据
return slot(props)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
插槽的 VNode 结构
javascript
// 子组件的 children 结构
const vnode = {
type: Child,
children: {
header: () => [h('h1', '标题')],
default: () => [h('p', '默认内容')],
footer: (props) => [h('span', `作者: ${props.userName}`)]
},
shapeFlag: ShapeFlags.COMPONENT | ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
插槽的渲染时机
javascript
// 子组件渲染时,会调用 renderSlot
function mountComponent(vnode) {
// setupComponent 中初始化 slots
initSlots(instance, children)
// 渲染函数中使用 renderSlot
const rendered = instance.render()
}
// 子组件的 render 函数
function render() {
return h('div', [
// 这里会调用 renderSlot
renderSlot(this.$slots, 'header'),
renderSlot(this.$slots, 'default'),
renderSlot(this.$slots, 'footer', { userName: this.author })
])
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
默认插槽
vue
<!-- 父组件 -->
<Child>
<p>默认内容</p>
</Child>
<!-- 编译后 -->
h(Child, null, {
default: () => [h('p', '默认内容')]
})1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
子组件使用默认插槽
vue
<template>
<div>
<slot></slot>
</div>
</template>1
2
3
4
5
2
3
4
5
具名插槽
vue
<!-- 父组件 -->
<Layout>
<template #header>
<h1>网站标题</h1>
</template>
<template #sidebar>
<nav>导航菜单</nav>
</template>
<template #footer>
<p>版权信息</p>
</template>
</Layout>1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
子组件模板
vue
<template>
<div class="layout">
<header><slot name="header"></slot></header>
<aside><slot name="sidebar"></slot></aside>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
</template>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
动态插槽名
vue
<!-- 父组件 -->
<template #[slotName]>
<p>动态插槽内容</p>
</template>
<!-- 编译后 -->
h(Child, null, {
[slotName]: () => [h('p', '动态插槽内容')]
})1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
插槽的嵌套
vue
<!-- 父组件 -->
<Outer>
<template #default>
<Inner>
<template #default>
<p>嵌套内容</p>
</template>
</Inner>
</template>
</Outer>1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
总结
| 概念 | 说明 |
|---|---|
#name | 具名插槽 |
#default | 默认插槽 |
v-slot | 插槽指令 |
renderSlot | 渲染插槽的函数 |
| 作用域插槽 | 插槽内容可以访问子组件数据 |