Vue 插槽实现原理笔记
一、核心思想
插槽的本质是父组件传递给子组件的、用于渲染 VNode 的函数。
- 父组件定义 如何渲染 (提供函数体)。
- 子组件决定 何时何地渲染 (调用函数)。
二、插槽的定义与调用
1. 子组件:定义插槽(挖坑)
在子组件的模板中,使用 <slot> 标签定义插槽的渲染位置。
- 具名作用域插槽:向父组件暴露数据。vue
<slot name="header" :title="'Hello Slot'"></slot> - 默认插槽:可以包含默认内容,在父组件未提供内容时显示。vue
<slot> <p>如果你没传,就会显示我这个默认内容</p> </slot> - 具名插槽:普通的命名插槽。vue
<slot name="footer"></slot>
2. 父组件:填充插槽(填坑)
在父组件中使用子组件时,通过 <template> 标签和 v-slot (简写 #) 指令来提供插槽内容。
<HelloWorld>
<template #header="{ title }">
<h1>{{ title }} - 这是父组件渲染的内容</h1>
</template>
<template #default>
<div>这是默认插槽的内容</div>
</template>
<template #footer>
<p>这是 Footer 的内容</p>
</template>
</HelloWorld>三、原理剖析:插槽即函数
1. 子组件如何“接收”插槽
在子组件的 setup 函数中,可以通过 useSlots (Composition API) 或 this.$slots (Options API) 来访问父组件传递过来的所有插槽。
slots 是一个对象,其 key 是插槽名,value 是一个函数。
import { useSlots } from 'vue';
export default {
setup() {
const slots = useSlots();
// 打印 slots 对象
console.log(slots);
// 输出:
// {
// header: (props) => [ VNode, ... ],
// default: () => [ VNode, ... ],
// footer: () => [ VNode, ... ]
// }
}
}2. 子组件如何“调用”插槽
子组件的模板最终会被编译成渲染函数。在渲染函数中,原本 <slot> 标签所在的位置会变成对 slots 对象中相应函数的调用。
<slot name="header" :title="..."></slot>编译为slots.header({ title: ... })<slot></slot>编译为slots.default()<slot name="footer"></slot>编译为slots.footer()
处理默认内容的逻辑:
// 如果父组件提供了 default 插槽,则调用它;否则渲染默认内容。
slots.default ? slots.default() : [ h('p', '这是默认内容') ];每个插槽函数执行后,都会返回一个由 VNode (虚拟节点) 组成的数组。
四、模板编译的视角
Vue 模板无法直接在浏览器中运行,它必须先被编译成 JavaScript(通常是 h() 函数或 createVNode 函数的调用)。
1. 父组件的编译结果
父组件在使用子组件并为其提供插槽时,会编译成类似下面的代码。本质是:渲染子组件时,将一个描述所有插槽的对象作为第三个参数传递进去。
import { h } from 'vue';
import HelloSlot from './HelloSlot.vue';
// 原始模板 <HelloWorld>...</HelloWorld>
// 编译后大致如下:
export default {
render() {
return h(
HelloSlot, // 1. 要渲染的子组件
null, // 2. Props (此处为 null)
{ // 3. Slots 对象
// header 插槽是一个函数,接收 title,返回一个 h1 VNode
header: ({ title }) => h('h1', null, title + ' - JS 组件'),
// default 插槽是一个函数,返回一个 div VNode
default: () => h('div', null, '这是默认插槽的内容'),
// footer 插槽是一个函数,返回一个 p VNode
footer: () => h('p', null, '这是 Footer 的内容')
}
);
}
}2. 子组件的编译结果
子组件的模板会编译成一个接收 props 和 context (包含slots) 的函数。它在自己的渲染逻辑中调用从 context.slots 中获取的函数。
import { h } from 'vue';
// 原始模板 <div class="child"> <slot name="header" ... /> ... </div>
// 编译后大致如下:
export default {
// context 对象里包含 attrs, emit, slots 等
setup(props, context) {
const { slots } = context;
return () => h('div', null, [
// 调用 header 函数并传入数据
slots.header({ title: 'Hello Slot' }),
// 调用 default 函数
slots.default ? slots.default() : h('p', '默认内容'),
// 调用 footer 函数
slots.footer()
]);
}
}五、延伸与对比
结论:任何能返回 VNode 的函数,都可以被 Vue 渲染。
插槽是父组件向子组件传递“渲染逻辑”(函数)的一种语法糖。我们也可以通过 props 传递一个函数来实现类似的效果。
示例:通过 Prop 传递一个返回 VNode 的函数。
// 父组件
<ChildComponent :renderFunc="() => h('div', null, '我是通过 Prop 渲染的')" />
// 子组件
export default {
props: {
renderFunc: Function
},
setup(props) {
// 直接调用 prop 里的函数即可渲染
return () => props.renderFunc();
}
}这证明了插槽的本质就是函数传递,无论是通过 slots 机制还是 props 机制,只要最终能拿到一个返回 VNode 的函数并执行它,Vue 就能将其渲染出来。