识别并初始化组件插槽,并通过 setupContext 暴露给子组件
2025-10-29 11:35:33
之前实现了组件事件的触发。
除了通过 props 传递数据,父组件还需要向子组件传递一段可以自定义的内容,这就是插槽。
import { createApp, h } from '../dist/vue.esm.js'
const Child = {
setup(_, { slots }) {
return () => {
return h('div', [
slots.header(),
slots.default(),
slots.footer({ c: 1 })
])
}
}
}
const Com = {
setup() {
return () => {
return h(Child, null, {
header: () => h('div', 'header'),
default: () => h('div', 'default'),
footer: ({ c }) => h('div', `footer-${c}`)
})
}
}
}
createApp(Com).mount('#app')
传递了三种插槽:
header:具名插槽default:默认插槽footer:作用域插槽
父组件通过 h 的第三个参数传递插槽,子组件通过 setup 的第二个参数访问 slots。
slots = {
header: () => h('div', 'header'),
default: () => h('div', 'default'),
footer: ({ c }) => h('div', `footer-${c}`)
}
所以插槽的本质就是一组返回虚拟节点的函数。
实现插槽
1. 识别插槽
之前处理 children 时,主要考虑了元素节点的文本和数组子节点。
但当 vnode 表示组件时,组件的 children 不再是普通的子节点,而是父组件传入的插槽。
因此,需要在 normalizeChildren 中根据 vnode 的类型标记插槽:
// packages/runtime-core/src/vnode.ts
function normalizeChildren(vnode, children) {
let { shapeFlag } = vnode
if (isArray(children)) {
shapeFlag |= ShapeFlags.ARRAY_CHILDREN
}
else if (isObject(children)) {
if (shapeFlag & ShapeFlags.COMPONENT) {
shapeFlag |= ShapeFlags.SLOTS_CHILDREN
}
}
else if (isFunction(children)) {
if (shapeFlag & ShapeFlags.COMPONENT) {
shapeFlag |= ShapeFlags.SLOTS_CHILDREN
children = {
default: children
}
}
}
else if (isString(children) || isNumber(children)) {
children = String(children)
shapeFlag |= ShapeFlags.TEXT_CHILDREN
}
vnode.children = children
vnode.shapeFlag = shapeFlag
return children
}
对于组件类型的 vnode:
- 如果
children是对象,直接将它标记为插槽 - 如果
children是函数,将它转换成default插槽
比如这种
h(Child, null, () => h('div', 'hello world'))
将转换成
{
default: () => h('div', 'hello world')
}
这样,无论父组件使用具名插槽还是默认插槽,内部都可以统一通过对象形式访问。
2. 初始化插槽
在 setupComponent 中初始化插槽:
// packages/runtime-core/src/component.ts
export function setupComponent(instance) {
initProps(instance)
initSlots(instance)
setupStatefulComponent(instance)
}
initSlots 只需要处理带有 SLOTS_CHILDREN 标记的组件:
// packages/runtime-core/src/componentSlots.ts
import { ShapeFlags } from '@vue/shared'
export function initSlots(instance) {
const { vnode, slots } = instance
if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const { children } = vnode
for (const key in children) {
slots[key] = children[key]
}
}
}
初始化完成后,组件实例中的 slots 大致是:
slots = {
header: () => h('div', 'header'),
default: () => h('div', 'default'),
footer: ({ c }) => h('div', `footer-${c}`)
}
3. 暴露给 setup
前面已经通过 setupContext 暴露了 attrs 和 emit。
现在把组件实例上的 slots 也放到 setupContext 中:
// packages/runtime-core/src/component.ts
function createSetupContext(instance) {
return {
get attrs() {
return instance.attrs
},
emit(event, ...args) {
emit(instance, event, ...args)
},
slots: instance.slots
}
}
这样,子组件就可以在 setup 中访问插槽:
const Child = {
setup(_, { slots }) {
return () => {
return h('div', [
slots.header(),
slots.default(),
slots.footer({ c: 1 })
])
}
}
}
调用插槽函数时,可以传入参数。父组件中的作用域插槽会接收到这些参数:
footer: ({ c }) => h('div', `footer-${c}`)
这就是作用域插槽的基本原理:数据由子组件提供,内容由父组件定义。