花3分钟来了解一下Vue3中的插槽到底是什么玩意

前言

插槽看着是一个比较神秘的东西,特别是作用域插槽还能让我们在父组件里面直接访问子组件里面的数据,这让插槽变得更加神秘了。其实Vue3的插槽远比你想象的简单,这篇文章我们来揭开插槽的神秘面纱。

欧阳也在找工作,坐标成都求内推!

看个demo

我们先来看个常见的插槽demo,其中子组件代码如下:

javascript 复制代码
<template>
  <slot></slot>
  <slot name="header"></slot>
  <slot name="footer" :desc="desc"></slot>
</template>

<script setup>
import { ref } from "vue";
const desc = ref("footer desc");
</script>

在子组件中我们定义了三个插槽,第一个是默认插槽,第二个是name为header的插槽,第三个是name为footer的插槽,并且将desc变量传递给了父组件。

我们再来看看父组件代码如下:

javascript 复制代码
<template>
  <ChildDemo>
    <p>default slot</p>
    <template v-slot:header>
      <p>header slot</p>
    </template>
    <template v-slot:footer="{ desc }">
      <p>footer slot: {{ desc }}</p>
    </template>
  </ChildDemo>
</template>

<script setup lang="ts">
import ChildDemo from "./child.vue";
</script>

在父组件中的代码很常规,分别使用v-slot指令给headerfooter插槽传递内容。

来看看编译后的父组件

我们在浏览器中来看看编译后的父组件代码,简化后如下:

javascript 复制代码
import {
  createBlock as _createBlock,
  createElementVNode as _createElementVNode,
  openBlock as _openBlock,
  toDisplayString as _toDisplayString,
  withCtx as _withCtx,
} from "/node_modules/.vite/deps/vue.js?v=64ab5d5e";

const _sfc_main = /* @__PURE__ */ _defineComponent({
  // ...省略
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock($setup["ChildDemo"], null, {
      header: _withCtx(
        () =>
          _cache[0] ||
          (_cache[0] = [
            _createElementVNode(
              "p",
              null,
              "header slot",
              -1
              /* HOISTED */
            ),
          ])
      ),
      footer: _withCtx(({ desc }) => [
        _createElementVNode(
          "p",
          null,
          "footer slot: " + _toDisplayString(desc),
          1
          /* TEXT */
        ),
      ]),
      default: _withCtx(() => [
        _cache[1] ||
          (_cache[1] = _createElementVNode(
            "p",
            null,
            "default slot",
            -1
            /* HOISTED */
          )),
      ]),
      _: 1,
      /* STABLE */
    })
  );
}
export default /* @__PURE__ */ _export_sfc(_sfc_main, [
  ["render", _sfc_render],
]);

从上面的代码可以看到template中的代码编译后变成了render函数。

在render函数中_createBlock($setup["ChildDemo"]表示在渲染子组件ChildDemo,并且在执行createBlock函数时传入了第三个参数是一个对象。对象中包含headerfooterdefault三个方法,这三个方法对应的是子组件ChildDemo`中的三个插槽。执行这三个方法就会生成这三个插槽对应的虚拟DOM。

并且我们观察到插槽footer处的方法还接收一个对象作为参数,并且对象中还有一个desc字段,这个字段就是子组件传递给父组件的变量。

方法最外层的withCtx方法是为了给插槽的方法注入当前组件实例的上下文。

通过上面的分析我们可以得出一个结论:在父组件中插槽经过编译后会变成一堆由插槽name组成的方法,执行这些方法就会生成插槽对应的虚拟DOM。默认插槽就是default方法,方法接收的参数就是子组件中插槽给父组件传递的变量。但是有一点要注意,在父组件中我们只是定义了这三个方法,执行这三个方法的地方却不是在父组件,而是在子组件。

编译后的子组件

我们来看看编译后的子组件,简化后代码如下:

javascript 复制代码
import {
  createElementBlock as _createElementBlock,
  Fragment as _Fragment,
  openBlock as _openBlock,
  renderSlot as _renderSlot,
} from "/node_modules/.vite/deps/vue.js?v=64ab5d5e";

const _sfc_main = {
  // ...省略
};

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock(
      _Fragment,
      null,
      [
        _renderSlot(_ctx.$slots, "default"),
        _renderSlot(_ctx.$slots, "header"),
        _renderSlot(_ctx.$slots, "footer", { desc: $setup.desc }),
      ],
      64 /* STABLE_FRAGMENT */
    )
  );
}

export default /*#__PURE__*/ _export_sfc(_sfc_main, [["render", _sfc_render]]);

同样的我们观察里面的render函数,里面的这个:

javascript 复制代码
[
  _renderSlot(_ctx.$slots, "default"),
  _renderSlot(_ctx.$slots, "header"),
  _renderSlot(_ctx.$slots, "footer", { desc: $setup.desc }),
]

对应的就是源代码里面的这个:

javascript 复制代码
<slot></slot>
<slot name="header"></slot>
<slot name="footer" :desc="desc"></slot>

在上面我们看见一个$slots对象,这个是什么东西呢?

其实useSlots就是返回的$slots对象,我们直接在控制台中使用useSlots打印出$slots对象看看,代码如下:

javascript 复制代码
<script setup>
import { ref, useSlots } from "vue";

const slots = useSlots();
console.log(slots);
const desc = ref("footer desc");
</script>

我们来浏览器中看看此时打印的slots对象是什么样的,如下图:

从上图中可以看到slots对象好像有点熟悉,这个对象中包含defaultfooterheader这三个方法,其实这个slots对象就是前面我们讲的父组件中定义的那个对象,执行对象的defaultfooterheader方法就会生成对应插槽的虚拟DOM。

前面我们讲了在父组件中会定义defaultfooterheader这三个方法,那这三个方法又是在哪里执行的呢?

答案是:在子组件里面执行的。

在执行_renderSlot(_ctx.$slots, "default")方法时就会去执行slots对象里面的default方法,这个是renderSlot函数的代码截图:

从上图中可以看到在renderSlot函数中首先会使用slots[name]拿到对应的插槽方法,如果执行的是_renderSlot(_ctx.$slots, "footer", { desc: $setup.desc }),这里拿到的就是footer方法。

然后就是执行footer方法,前面我们讲过了这里的footer方法需要接收参数,并且从参数中结构出desc属性。刚好我们执行renderSlot方法时就给他传了一个对象,对象中就有一个desc属性,这不就对上了吗!

并且由于执行footer方法会生成虚拟DOM,所以footer生成的虚拟DOM是属于子组件里面的,同理footer对应的真实DOM也是属于在子组件的DOM树里面。

通过上面的分析我们可以得出一个结论就是:子组件中的插槽实际就是在执行父组件插槽对应的方法,在执行方法时可以将子组件的变量传递给父组件,这就是作用域插槽的原理。

总结

这篇文章我们讲了经过编译后父组件的插槽会被编译成一堆方法,这些方法组成的对象就是$slots对象。在子组件中会去执行这些方法,并且可以将子组件的变量传给父组件,由父组件去接收参数,这就是作用域插槽的原理。了解了这个后当我们在useSlotsjsxtsx中定义和使用插槽就不会那么迷茫了。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会