本文系统梳理了 Slot(插槽)的概念、使用场景、分类、实现原理,并对比了 Vue 2 与 Vue 3 在插槽机制上的异同。文中包含示例代码与底层实现要点,适合对 Vue 插槽原理与实战有中高级需求的读者阅读。
一、什么是 slot
slot
最早来自 Web Components(原生自定义元素)规范,是组件内部的占位符,用于在组件外部填充内容。原生 HTML 的一个例子:
xml
<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:
scala
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
:
xml
<template>
<slot>
<p>插槽后备的内容</p>
</slot>
</template>
父组件使用:
xml
<Child>
<div>默认插槽</div>
</Child>
具名插槽
通过 name
属性为插槽命名,父组件使用 v-slot:slotName
或老语法 slot="name"
对应插入。
子组件:
xml
<template>
<slot>插槽后备的内容</slot>
<slot name="content">插槽后备的内容</slot>
</template>
父组件:
xml
<child>
<template v-slot:default>具名插槽</template>
<template v-slot:content>内容...</template>
</child>
(Vue 支持 v-slot
的缩写 #
,例如 #content
)
作用域插槽(Scoped Slot)
作用域插槽允许子组件"向父组件传值"。子组件在 <slot>
上绑定要传递的属性,父组件通过 v-slot
(或 #
)接收这个对象并在插槽模板中使用。
子组件 Child.vue
:
xml
<template>
<slot name="footer" testProps="子组件的值">
<h3>没传footer插槽</h3>
</slot>
</template>
父组件:
xml
<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。
举例:
xml
Vue.component('button-counter', {
template: '<div> <slot>我是默认内容</slot></div>'
})
new Vue({
el: '#app',
template: '<button-counter><span>我是slot传入内容</span></button-counter>',
components:{buttonCounter}
})
调用 buttonCounter
的编译后渲染函数(简化):
javascript
(function anonymous() {
with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})
_v
:创建普通文本节点_t
:渲染插槽的函数(render slot)
渲染插槽的实现(简化版)为 renderSlot
:
ini
function renderSlot(name, fallback, props, bindObject) {
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
nodes = scopedSlotFn(props) || fallback;
return nodes;
}
renderSlot
会:
- 先找
this.$scopedSlots[name]
(父组件传过来的渲染函数)并执行,得到 nodes; - 如果没有传渲染函数,则使用
fallback
(子组件<slot>
内的默认内容)。
那么 this.$scopedSlots
、vm.$slots
从何来?在 initRender(vm)
阶段:
ini
function initRender (vm) {
...
vm.$slots = resolveSlots(options._renderChildren, renderContext);
...
}
resolveSlots
会把父组件传入子组件的 children 按 slot
属性分类到不同的 key(例如 default
或 header
、footer
等),并返回一个对象:
ini
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.$slots
、this.$scopedSlots
(2.x)访问。 - Vue 3(Composition API) :在
setup(props, { slots })
的第二个参数中可直接拿到slots
,slots
中的每一项是一个函数:
javascript
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 以后即可使用,但在老项目中仍可能见到slot
与slot-scope
的写法(需要迁移时注意替换)。- Vue 3 下,插槽函数返回值应注意不要返回
undefined
,而应返回null
或空数组以避免运行时错误。 - 在 Vue 3 中,若要在
render
函数或 JSX 中使用插槽,直接调用slots.mySlot?.(props)
即可。
六、实战建议与常见坑
- 优先使用
v-slot
语法(统一且清晰),在只有默认插槽时可以直接在组件标签上书写(语法糖)。 - 谨慎向插槽传递过多数据 :插槽最适合作为模板分发与少量数据传递;如果要传递大量交互逻辑或状态,考虑使用
provide/inject
或把数据提升到父组件后通过 props/事件交互。 - 给插槽提供后备内容(fallback) ,保证在父组件不传入内容时组件依然有合理表现。
- 注意性能:复杂的插槽内容应尽量避免频繁创建新的对象/回调;在 Vue 3 中利用编译器的静态提升和缓存可以获得更好性能。
- 兼容老代码 :从 Vue 2 迁移到 Vue 3 时,先把
slot-scope
/slot
替换为v-slot
,并把this.$scopedSlots
的使用迁移为函数式的this.$slots
或在setup
中使用slots
。
七、结论
插槽(Slot)是组件化 UI 的一大利器,通过将可变内容与组件模板解耦,Slot 能显著提升组件的复用性与灵活度。理解插槽的底层实现(父组件内容如何被收集、归类,再由子组件在渲染阶段以渲染函数的形式执行)有助于写出更稳定、更可维护的组件。
随着 Vue 的演进,插槽实现从 "VNode 数组 + 渲染函数同时存在" 的混合表示,走向了 Vue 3 更统一的函数化表示,这带来了更好的编译优化、类型支持与运行时性能。
本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。