Vue 插槽 (slot) 的实现原理
前言
大家好,我是你们的友好邻居 Talon ,想必大家都用过 Vue 的插槽吧,用过的人都说好用,没用过的人建议用一用,那么这个插槽到底是怎么实现的,我们今天就来一探究竟。
插槽的工作原理与实现
顾名思义,组件的插槽指组件会预留一个槽位,该槽位具体要渲 染的内容由用户插入,如下面给出的 MyComponent
组件的模板所 示:
<template>
<header>
<slot name="header"/>
</header>
<div>
<slot name="body"/>
</div>
<footer>
<slot name="footer"/>
</footer>
</template>
<template>
<header>
<slot name="header"/>
</header>
<div>
<slot name="body"/>
</div>
<footer>
<slot name="footer"/>
</footer>
</template>
当在父组件中使用 <MyComponent>
组件时,可以根据插槽的名 字来插入自定义的内容:
<MyComponent>
<template #header>
<h1>我是标题</h1>
</template>
<template #body>
<section>我是内容</section>
</template>
<template #footer>
<p>我是注脚</p>
</template>
</MyComponent>
<MyComponent>
<template #header>
<h1>我是标题</h1>
</template>
<template #body>
<section>我是内容</section>
</template>
<template #footer>
<p>我是注脚</p>
</template>
</MyComponent>
上面这段父组件的模板会被编译成如下渲染函数
// 父组件的渲染函数
function render() {
return {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() {
return {type: 'h1', children: '我是标题'}
},
body() {
return {type: 'section', children: '我是内容'}
},
footer() {
return {type: 'p', children: '我是注脚'}
}
}
}
}
// 父组件的渲染函数
function render() {
return {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() {
return {type: 'h1', children: '我是标题'}
},
body() {
return {type: 'section', children: '我是内容'}
},
footer() {
return {type: 'p', children: '我是注脚'}
}
}
}
}
可以看到,组件模板中的插槽内容会被编译为插槽函数,而插槽 函数的返回值就是具体的插槽内容。组件 MyComponent 的模板则会 被编译为如下渲染函数:
// MyComponent 组件模板的编译结果
function render() {
return [
{
type: 'header',
children: [this.$slots.header()]
},
{
type: 'body',
children: [this.$slots.body()]
},
{
type: 'footer',
children: [this.$slots.footer()]
}
]
}
// MyComponent 组件模板的编译结果
function render() {
return [
{
type: 'header',
children: [this.$slots.header()]
},
{
type: 'body',
children: [this.$slots.body()]
},
{
type: 'footer',
children: [this.$slots.footer()]
}
]
}
可以看到,渲染插槽内容的过程,就是调用插槽函数并渲染由其 返回的内容的过程。这与 React 中 render props 的概念非常相似。 在运行时的实现上,插槽则依赖于 setupContext 中的 slots 对象,如下面的代码所示:
function mountComponent(vnode, container, anchor) {
// 省略部分代码
// 直接使用编译好的 vnode.children 对象作为 slots 对象即可
const slots = vnode.children || {}
// 将 slots 对象添加到 setupContext 中
const setupContext = {attrs, emit, slots}
}
function mountComponent(vnode, container, anchor) {
// 省略部分代码
// 直接使用编译好的 vnode.children 对象作为 slots 对象即可
const slots = vnode.children || {}
// 将 slots 对象添加到 setupContext 中
const setupContext = {attrs, emit, slots}
}
可以看到,最基本的 slots 的实现非常简单。只需要将编译好的 VNode.children 作为 slots 对象,然后将 slots 对象添加到 setupContext 对象中。为了在 render 函数内和生命周期钩子函数 内能够通过 this.$slots 来访问插槽内容,我们还需要在 renderContext 中特殊对待 $slots 属性,如下面的代码所示:
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const slots = vnode.children || {}
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
// 将插槽添加到组件实例上
slots
}
// 省略部分代码
const renderContext = new Proxy(instance, {
get(t, k, r) {
const {state, props, slots} = t
// 当 k 的值为 $slots 时,直接返回组件实例上的 slots
if (k === '$slots') return slots
// 省略部分代码
},
set(t, k, v, r) {
// 省略部分代码
}
})
// 省略部分代码
}
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const slots = vnode.children || {}
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
// 将插槽添加到组件实例上
slots
}
// 省略部分代码
const renderContext = new Proxy(instance, {
get(t, k, r) {
const {state, props, slots} = t
// 当 k 的值为 $slots 时,直接返回组件实例上的 slots
if (k === '$slots') return slots
// 省略部分代码
},
set(t, k, v, r) {
// 省略部分代码
}
})
// 省略部分代码
}
我们对渲染上下文 renderContext 代理对象的 get 拦截函数做 了特殊处理,当读取的键是 $slots 时,直接返回组件实例上的 slots 对象,这样用户就可以通过 this.$slots 来访问插槽内容 了。
—— 节选自 Vue.js 设计与实现
总结
当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在 vm.$slot中,默认插槽为vm.$slot.default,具名插槽为 vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
最后
好了,今天就到这里啦,明天依旧光芒万丈哦~,希望对你们有帮助,祝大家工作顺利,生活愉快! 读者有什么更好的方式想法,欢迎留言评论,一起学习一起进步!