Skip to content

✨ 插槽的本质 👌

要点速览

  • 插槽集合在编译后本质是“以插槽名为键,值为函数的对象”,父组件以“具名函数”形式传递模板内容给子组件。
  • 默认插槽、具名插槽、作用域插槽,最终都以“函数返回 VNode(或 VNode 数组)”呈现。
  • 子组件通过调用 slots.xxx(props) 取得要渲染的虚拟 DOM;作用域插槽的 props 由子组件提供。

概念回顾

  • 子组件:通过 slot(或在 SFC 模板中的 <slot/>)设置插槽位。
  • 父组件:使用子组件时,将模板内容以“插槽函数”的形式传递给子组件。

使用层面的本质

插槽是“父向子传递模板内容”的机制,但在运行期,传递的是“具名函数”,调用后返回要渲染的 VNode 结构。

父组件传递内容的本质

父组件传的是一个“以插槽名为键的函数对象”:

jsx
{
  default: function(){ /* 返回默认插槽的 VNode */ },
  header: function(){ /* 返回具名插槽 header 的 VNode */ },
  // ...
}

例如:

jsx
{
  default: function(){
    // 注意:返回的是虚拟 DOM(可为数组)
    return (
      <div class="card-content">
        <img src="./assets/landscape.jpeg" alt="Beautiful landscape" class="card-image" />
        <p>探索未知的自然风光,记录下每一个令人惊叹的瞬间。加入我们的旅程,一起见证世界的壮丽。</p>
      </div>
    )
  },
  header: function(){
    return (<div>摄影作品</div>)
  }
}

常见误区(父侧)

  • 以为传的是“DOM 片段”,其实是“函数”;需要调用才能得到 VNode。
  • 忘记具名:具名插槽需与子侧插槽名一致,否则子组件取不到对应函数。

子组件渲染插槽的本质

子组件拿到父传的 slots 对象,调用其中的函数以获得要渲染的 VNode:

jsx
const slots = {
  default: function () {
    /* ... */
  },
  header: function () {
    /* ... */
  },
};
// 该对象由父组件传入(在 SFC 中通过 setup 的第二个参数使用)
slots.default(); // 得到默认插槽的 VNode
slots.header(); // 得到 header 插槽的 VNode

对于“作用域插槽”,子组件向插槽函数传递 props

jsx
slots.header({ title: "子组件提供的标题" });

常见误区(子侧)

  • slots.default 可能不存在,需要判空再调用。
  • 插槽函数可能返回数组或空数组,需要做好结果形态的兼容处理。
  • 作用域插槽的数据应由子组件提供,父组件在插槽函数体内使用这些 props

验证示例

下面的示例展示了子组件如何调用默认插槽与具名插槽(含作用域插槽):

子组件

vue
<script>
import { defineComponent, h, ref } from "vue";
import styles from "./CardComponent.module.css";

export default defineComponent({
  name: "CardComponent",
  setup(_, { slots }) {
    // 通过setup函数拿到slots对象,它是一个以插槽名为键的函数对象,每个函数返回要渲染的VNode(或VNode数组)

    const title = ref("这是子组件标题");
    // 调用默认插槽函数,若不存在则返回空数组
    const defaultSlotsVNode = slots.default?.() ?? []; // 使用可选操作符和空数组合并运算符,避免null或undefined
    let headerSlotVNode = null;

    if (slots.header) {
      // 调用具名插槽函数header,若不存在则返回null
      headerSlotVNode = slots.header({ title: title.value });
    }
    if (
      !headerSlotVNode ||
      (Array.isArray(headerSlotVNode) && headerSlotVNode.length === 0)
    ) {
      headerSlotVNode = h("div", null, "默认标题");
    }

    return () =>
      h("div", { class: styles.card }, [
        h("div", { class: styles["card-header"] }, headerSlotVNode),
        h("div", { class: styles["card-body"] }, defaultSlotsVNode),
      ]);
  },
});
</script>

父组件

vue
<script>
import { defineComponent, h } from "vue";
import CardComponent from "./CardComponent";

export default defineComponent({
  name: "ParentComponent",
  setup() {
    // setup 可以返回一个渲染函数,用于直接生成该组件的 VNode 树并填充插槽内容。
    return () =>
      // 渲染CardComponent组件,并且传出header插槽和默认插槽
      h(CardComponent, null, {
        header: ({ title }) => h("h3", null, title),
        default: () => [h("p", null, "这里是默认插槽的内容")],
      });
  },
});
</script>
说明
  • slots.default?.() 使用可选链,避免未传默认插槽时报错。
  • 作用域插槽通过参数向父组件传递数据(此示例为 title)。
  • 返回结果可能为空或数组,需做好兜底与形态兼容。

小结

总结

  • 插槽集合在运行期时是“以插槽名为键,值为函数的对象”,父侧提供函数,子侧调用得到 VNode。
  • 作用域插槽由子组件提供数据 props,父组件在插槽函数体内使用。
  • 判空与结果形态兼容是插槽渲染常见的工程化要点。