12.7 插槽的工作原理与实现
插槽就是组件预留的槽位:内容在编译阶段变成「返回虚拟 DOM 的函数」放进组件的
children,子组件渲染它,就是调用函数再 patch。
一、插槽内容的编译结果
<MyComponent>
<template #header><h1>我是标题</h1></template>
<template #body><section>我是内容</section></template>
<template #footer><p>我是注脚</p></template>
</MyComponent>
编译后,组件的 children 是一个对象,每个属性对应一个插槽函数:
const CompVNode = {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() { return { type: 'h1', children: '我是标题' } },
body() { return { type: 'section', children: '我是内容' } },
footer() { return { type: 'p', children: '我是注脚' } }
}
}
插槽函数就是「返回虚拟 DOM 的函数」,它的返回值就是具体的插槽内容;渲染插槽内容 = 调用插槽函数 + 渲染其返回值。 插槽内容是父组件模板的一部分,但由子组件决定在什么位置、什么时机渲染。
二、运行时:slots 直接取自 vnode.children
// 直接使用编译好的 vnode.children 对象作为 slots 对象
const slots = vnode.children || {}
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots
}
// 放进 setupContext,setup(props, { slots }) 即可取用
const setupContext = { attrs, emit, slots }
三、渲染上下文里特殊处理 $slots
const renderContext = new Proxy(instance, {
get(t, k) {
const { state, props, slots } = t
if (k === '$slots') return slots // 特判:$slots 指向实例的 slots
if (state && k in state) {
return state[k]
} else if (k in props) {
return props[k]
} else if (setupState && k in setupState) {
return setupState[k]
} else {
console.error('不存在')
}
}
// set 不变
})
$slots不是实例自身的属性,所以要在代理的get里特判,才能让渲染函数和生命周期钩子里都能写this.$slots.header()。
四、子组件如何渲染插槽
render() {
return {
type: Fragment,
children: [
{ type: 'header', children: [ this.$slots.header() ] },
{ type: 'body', children: [ this.$slots.body() ] },
{ type: 'body', children: this.bar },
{ type: 'footer', children: [ this.$slots.footer() ] }
]
}
}
| 形态 | 说明 |
|---|---|
| 编译产物 | vnode.children 是「插槽名 → 插槽函数」的对象 |
| 运行时 | setupContext.slots 与 this.$slots 指向同一个对象 |
| 渲染 | 调用 this.$slots.xxx() 得到虚拟 DOM,再交给 patch |
插槽函数是在子组件的渲染过程中被调用的,因此它读取的响应式数据会被「当前这次渲染的副作用函数」收集。 本例没有处理插槽本身的更新:子组件被动更新时只更新了 props,slots 尚未处理。
相关
- 12.6 组件事件与 emit 的实现:setupContext 里的 emit
- 12.8 注册生命周期:用
currentInstance把组合式 API 的钩子与组件实例关联起来