12.7-插槽的工作原理与实现

98 阅读1分钟

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

<template>
  <header>
    <slot name="header" />
  </header>
  <div>
    <slot name="body" />
  </div>
  <footer>
    <slot name="footer" />
  </footer>
</template>

当在父组件中使用 组件时,可以根据插槽的名 字来插入自定义的内容:

<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: "我是注脚" }
      },
    },
  }
}

可以看到,组件模板中的插槽内容会被编译为插槽函数,而插槽 函数的返回值就是具体的插槽内容。组件 MyComponent 的模板则会 被编译为如下渲染函数:

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

可以看到,最基本的 slots 的实现非常简单。只需要将编译好的 vnode.children 作为 slots 对象,然后将 slots 对象添加到 setupContext 对象中。为了在 render 函数内和生命周期钩子函数 内能够通过 this.slots来访问插槽内容,我们还需要在renderContext中特殊对待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) {
      // 省略部分代码
    },
  })

  // 省略部分代码
}

我们对渲染上下文 renderContext 代理对象的 get 拦截函数做 了特殊处理,当读取的键是 slots时,直接返回组件实例上的slots对象,这样用户就可以通过this.slots 时,直接返回组件实例上的 slots 对象,这样用户就可以通过 this.slots 来访问插槽内容 了。