Skip to content

Vue 插槽 (slot) 的实现原理

前言

大家好,我是你们的友好邻居 Talon ,想必大家都用过 Vue 的插槽吧,用过的人都说好用,没用过的人建议用一用,那么这个插槽到底是怎么实现的,我们今天就来一探究竟。

插槽的工作原理与实现

顾名思义,组件的插槽指组件会预留一个槽位,该槽位具体要渲 染的内容由用户插入,如下面给出的 MyComponent 组件的模板所 示:

vue

<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> 组件时,可以根据插槽的名 字来插入自定义的内容:

vue

<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>

上面这段父组件的模板会被编译成如下渲染函数

js
// 父组件的渲染函数
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 的模板则会 被编译为如下渲染函数:

js
 // 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 对象,如下面的代码所示:

js
 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 属性,如下面的代码所示:

js
 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 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

最后

好了,今天就到这里啦,明天依旧光芒万丈哦~,希望对你们有帮助,祝大家工作顺利,生活愉快! 读者有什么更好的方式想法,欢迎留言评论,一起学习一起进步!