一、什么是 slot

slot 最早来自 Web Components(原生自定义元素)规范,是组件内部的占位符,用于在组件外部填充内容。原生 HTML 的一个例子:

<template id="element-details-template">
  <slot name="element-name">Slot template</slot>
</template>

<element-details>
  <span slot="element-name">1</span>
</element-details>

<element-details>
  <span slot="element-name">2</span>
</element-details>

template 本身不会直接渲染到页面,需要通过 JS 获取它并挂载到自定义元素的 shadow DOM:

customElements.define('element-details',
  class extends HTMLElement {
    constructor() {
      super();
      const template = document
        .getElementById('element-details-template')
        .content;
      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(template.cloneNode(true));
    }
  }
)

在 Vue 中,slot 的概念与此类似:子组件在模板中预留“坑位”,父组件在使用该组件时把需要的内容塞入组件标签内部,Vue 会把父组件传入的内容“分发”到子组件对应的插槽位置。

一个通俗比喻:插槽像是插卡式游戏机的卡槽,组件暴露插槽,用户可以插入不同的“游戏卡带”(自定义内容)来改变显示的内容。

二、使用场景

插槽能使组件更可扩展、可复用并允许父组件对组件内部特定位置进行定制:

  • 通用布局组件(Header / Footer / Sidebar 可定制)
  • 表格/列表的列模板(列内自定义渲染)
  • 弹窗的显示内容(标题、正文、底部按钮等可插入)
  • 下拉选项、卡片组件等需要自定义子结构的场景

当一个复用组件在不同地方仅需局部差异化处理时,插槽比“为每个场景复制组件”更优。父组件可以在不修改子组件代码的情况下,为子组件插入任意 DOM 或组件实例。

三、分类

通常把插槽分为三类:默认插槽、具名插槽、作用域插槽(scoped slot)。下面分别说明并给出示例。

默认插槽

子组件用 <slot> 标签确定渲染位置,标签内可写后备内容(fallback)。当父组件未传入内容时,显示后备内容;若传入,则替换显示父组件的内容。

子组件 Child.vue

<template>
  <slot>
    <p>插槽后备的内容</p>
  </slot>
</template>

父组件使用

<Child>
  <div>默认插槽</div>  
</Child>

具名插槽

通过 name 属性为插槽命名,父组件使用 v-slot:slotName 或老语法 slot="name" 对应插入。

子组件

<template>
  <slot>插槽后备的内容</slot>
  <slot name="content">插槽后备的内容</slot>
</template>

父组件

<child>
  <template v-slot:default>具名插槽</template>
  <template v-slot:content>内容...</template>
</child>

(Vue 支持 v-slot 的缩写 #,例如 #content

作用域插槽(Scoped Slot)

作用域插槽允许子组件“向父组件传值”。子组件在 <slot> 上绑定要传递的属性,父组件通过 v-slot(或 #)接收这个对象并在插槽模板中使用。

子组件 Child.vue

<template>
  <slot name="footer" testProps="子组件的值">
    <h3>没传footer插槽</h3>
  </slot>
</template>

父组件

<child>
  <template v-slot:default="slotProps">
    来自子组件数据:{{ slotProps.testProps }}
  </template>

  <!-- 等价的缩写 -->
  <template #default="slotProps">
    来自子组件数据:{{ slotProps.testProps }}
  </template>
</child>

小结要点

  • v-slot 只能放在 <template> 上。但当只有默认插槽时可以直接写在组件标签上(语法糖)。
  • 默认插槽的名字为 default,写 v-slot 时可以省略 default
  • # 缩写不能省略参数(写成 #default#+参数),可以使用解构:v-slot="{ user }",也可重命名或给默认值:v-slot="{ user = '默认值' }"

四、原理分析(Vue 插槽的底层实现要点)

在 Vue 中,组件渲染走的是:template -> render function -> VNode -> DOM
插槽本质是“返回 VNode 的函数(slot 渲染函数)”,在编译阶段会把插槽内容提取到父作用域,并在子组件执行插槽渲染函数时生成对应的 VNode。

举例:

Vue.component('button-counter', {
  template: '<div> <slot>我是默认内容</slot></div>'
})

new Vue({
  el: '#app',
  template: '<button-counter><span>我是slot传入内容</span></button-counter>',
  components:{buttonCounter}
})

调用 buttonCounter 的编译后渲染函数(简化):

(function anonymous() {
  with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})
  • _v:创建普通文本节点
  • _t:渲染插槽的函数(render slot)

渲染插槽的实现(简化版)为 renderSlot

function renderSlot(name, fallback, props, bindObject) {
  var scopedSlotFn = this.$scopedSlots[name];
  var nodes;
  nodes = scopedSlotFn(props) || fallback;
  return nodes;
}

renderSlot 会:

  1. 先找 this.$scopedSlots[name](父组件传过来的渲染函数)并执行,得到 nodes;
  2. 如果没有传渲染函数,则使用 fallback(子组件 <slot> 内的默认内容)。

那么 this.$scopedSlotsvm.$slots 从何来?在 initRender(vm) 阶段:

function initRender (vm) {
  ...
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  ...
}

resolveSlots 会把父组件传入子组件的 children 按 slot 属性分类到不同的 key(例如 defaultheaderfooter 等),并返回一个对象:

function resolveSlots(children, context) {
  if (!children || !children.length) {
    return {}
  }
  var slots = {};
  for (var i = 0, l = children.length; i < l; i++) {
    var child = children[i];
    var data = child.data;
    if (data && data.attrs && data.attrs.slot) {
      delete data.attrs.slot;
    }
    if ((child.context === context || child.fnContext === context) && data && data.slot != null) {
      var name = data.slot;
      var slot = (slots[name] || (slots[name] = []));
      if (child.tag === 'template') {
        slot.push.apply(slot, child.children || []);
      } else {
        slot.push(child);
      }
    } else {
      (slots.default || (slots.default = [])).push(child);
    }
  }
  // 去除只包含空白的 slot
  for (var name$1 in slots) {
    if (slots[name$1].every(isWhitespace)) {
      delete slots[name$1];
    }
  }
  return slots
}

最后,在渲染阶段会通过 normalizeScopedSlots 将这些 vm.$slots 转为 vm.$scopedSlots(渲染函数形式),供 renderSlot 调用。

作用域插槽能够让父组件接收到子组件传递的数据,是因为在 renderSlot 调用时会把 props 传给父组件提供的渲染函数(即上文 _t 的第三个参数)

五、Vue 2 与 Vue 3 在插槽上的区别与演进

下面列出 Vue 2 与 Vue 3 在插槽机制上的主要区别与演化点,重点在语法、内部实现、性能与组合式 API 下的用法差异。

1) 语法与使用层面的演进

  • v-slot 语法:

    • v-slot 是在 Vue 2.6 引入的统一插槽语法(替代早期的 slot / slot-scope 写法)。因此在 Vue 2.6+ 与 Vue 3 中,推荐使用 v-slot(或其缩写 #)。
    • 语法在 Vue 3 中保持一致,Vue 3 支持更灵活的解构和默认值写法(与 JS 语法一致)。
  • 默认插槽/具名插槽在使用上没有本质差异,但 Vue 3 对编译输出和运行时的 slot 表示更“函数化”。

2) 插槽的内部表示:函数化(Vue 3 更明确)

  • Vue 2:有 $slots(VNode 数组)和 $scopedSlots(渲染函数)两套概念。Vue 会在运行时把 slots 转换并归类,$scopedSlots 保存渲染函数供子组件调用。
  • Vue 3:插槽被设计为始终是函数slots 是一组返回 VNode 的函数),渲染层直接调用这些函数来获取节点。因为插槽是函数,所以更易于静态提升、Tree-shaking 与编译时优化,也更利于 TypeScript 类型推导。

影响:在 Vue 3 中没有单独的 $scopedSlots 区分(开发者通常直接使用 $slots,而 $slots.someSlot 是个函数)。这使得插槽更加统一和简单。

3) Composition API(setup)下的插槽获取方式

  • Vue 2(Options API) :通过 this.$slotsthis.$scopedSlots(2.x)访问。
  • Vue 3(Composition API) :在 setup(props, { slots }) 的第二个参数中可直接拿到 slotsslots 中的每一项是一个函数:
export default {
  setup(props, { slots }) {
    // slots.header() -> 返回 VNode 数组
  }
}

这使得在 setup 中处理插槽更自然、类型更明确。

4) 性能与编译优化

  • Vue 3 的编译器会尽可能把静态内容与插槽内容进行提升(static hoisting),并将可预测的插槽转换为常量引用或缓存的函数,从而减少重复渲染开销。
  • 插槽作为函数的表示也使得渲染器能更精确地控制何时重新求值从而减少不必要的 VNode 创建。

5) Fragment、多根与根节点限制的影响

  • Vue 2 要求组件有单一根节点,因此某些场景下需要额外包装元素,影响插槽的结构与样式。
  • Vue 3 支持 Fragment(多根节点),插槽内容与宿主组件之间的 DOM 关系更自由,父组件插入多个根节点到子组件插槽变得自然且语义清晰。

6) API 与移除的旧属性

  • Vue 3 将一些老旧 API 清理(例如:$scopedSlots 在很多场合不再是必须),开发者应使用 slots 函数形式或 Composition API 的 slots

7) TypeScript 支持更好

  • 由于 Vue 3 将插槽视为函数,配合 defineComponent 与类型声明,开发者可以更精确地为插槽定义类型(比如 slots: { default?: (props: { user: User }) => VNode[] }),这在大型项目中非常有价值。

8) 小差异与注意点(实践)

  • v-slot 在 Vue 2.6 以后即可使用,但在老项目中仍可能见到 slotslot-scope 的写法(需要迁移时注意替换)。
  • Vue 3 下,插槽函数返回值应注意不要返回 undefined,而应返回 null 或空数组以避免运行时错误。
  • 在 Vue 3 中,若要在 render 函数或 JSX 中使用插槽,直接调用 slots.mySlot?.(props) 即可。

六、实战建议与常见坑

  1. 优先使用 v-slot 语法(统一且清晰),在只有默认插槽时可以直接在组件标签上书写(语法糖)。
  2. 谨慎向插槽传递过多数据:插槽最适合作为模板分发与少量数据传递;如果要传递大量交互逻辑或状态,考虑使用 provide/inject 或把数据提升到父组件后通过 props/事件交互。
  3. 给插槽提供后备内容(fallback) ,保证在父组件不传入内容时组件依然有合理表现。
  4. 注意性能:复杂的插槽内容应尽量避免频繁创建新的对象/回调;在 Vue 3 中利用编译器的静态提升和缓存可以获得更好性能。
  5. 兼容老代码:从 Vue 2 迁移到 Vue 3 时,先把 slot-scope/slot 替换为 v-slot,并把 this.$scopedSlots 的使用迁移为函数式的 this.$slots 或在 setup 中使用 slots

七、结论

插槽(Slot)是组件化 UI 的一大利器,通过将可变内容与组件模板解耦,Slot 能显著提升组件的复用性与灵活度。理解插槽的底层实现(父组件内容如何被收集、归类,再由子组件在渲染阶段以渲染函数的形式执行)有助于写出更稳定、更可维护的组件。

随着 Vue 的演进,插槽实现从 "VNode 数组 + 渲染函数同时存在" 的混合表示,走向了 Vue 3 更统一的函数化表示,这带来了更好的编译优化、类型支持与运行时性能。


本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]