手机亦云小慧
117.33MB · 2025-12-20
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 会把父组件传入的内容“分发”到子组件对应的插槽位置。
一个通俗比喻:插槽像是插卡式游戏机的卡槽,组件暴露插槽,用户可以插入不同的“游戏卡带”(自定义内容)来改变显示的内容。
插槽能使组件更可扩展、可复用并允许父组件对组件内部特定位置进行定制:
当一个复用组件在不同地方仅需局部差异化处理时,插槽比“为每个场景复制组件”更优。父组件可以在不修改子组件代码的情况下,为子组件插入任意 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)
作用域插槽允许子组件“向父组件传值”。子组件在 <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 中,组件渲染走的是: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 会:
this.$scopedSlots[name](父组件传过来的渲染函数)并执行,得到 nodes;fallback(子组件 <slot> 内的默认内容)。那么 this.$scopedSlots、vm.$slots 从何来?在 initRender(vm) 阶段:
function initRender (vm) {
...
vm.$slots = resolveSlots(options._renderChildren, renderContext);
...
}
resolveSlots 会把父组件传入子组件的 children 按 slot 属性分类到不同的 key(例如 default 或 header、footer 等),并返回一个对象:
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 在插槽机制上的主要区别与演化点,重点在语法、内部实现、性能与组合式 API 下的用法差异。
v-slot 语法:
v-slot 是在 Vue 2.6 引入的统一插槽语法(替代早期的 slot / slot-scope 写法)。因此在 Vue 2.6+ 与 Vue 3 中,推荐使用 v-slot(或其缩写 #)。默认插槽/具名插槽在使用上没有本质差异,但 Vue 3 对编译输出和运行时的 slot 表示更“函数化”。
$slots(VNode 数组)和 $scopedSlots(渲染函数)两套概念。Vue 会在运行时把 slots 转换并归类,$scopedSlots 保存渲染函数供子组件调用。slots 是一组返回 VNode 的函数),渲染层直接调用这些函数来获取节点。因为插槽是函数,所以更易于静态提升、Tree-shaking 与编译时优化,也更利于 TypeScript 类型推导。影响:在 Vue 3 中没有单独的 $scopedSlots 区分(开发者通常直接使用 $slots,而 $slots.someSlot 是个函数)。这使得插槽更加统一和简单。
this.$slots、this.$scopedSlots(2.x)访问。setup(props, { slots }) 的第二个参数中可直接拿到 slots,slots 中的每一项是一个函数:export default {
setup(props, { slots }) {
// slots.header() -> 返回 VNode 数组
}
}
这使得在 setup 中处理插槽更自然、类型更明确。
$scopedSlots 在很多场合不再是必须),开发者应使用 slots 函数形式或 Composition API 的 slots。defineComponent 与类型声明,开发者可以更精确地为插槽定义类型(比如 slots: { default?: (props: { user: User }) => VNode[] }),这在大型项目中非常有价值。v-slot 在 Vue 2.6 以后即可使用,但在老项目中仍可能见到 slot 与 slot-scope 的写法(需要迁移时注意替换)。undefined,而应返回 null 或空数组以避免运行时错误。render 函数或 JSX 中使用插槽,直接调用 slots.mySlot?.(props) 即可。v-slot 语法(统一且清晰),在只有默认插槽时可以直接在组件标签上书写(语法糖)。provide/inject 或把数据提升到父组件后通过 props/事件交互。slot-scope/slot 替换为 v-slot,并把 this.$scopedSlots 的使用迁移为函数式的 this.$slots 或在 setup 中使用 slots。插槽(Slot)是组件化 UI 的一大利器,通过将可变内容与组件模板解耦,Slot 能显著提升组件的复用性与灵活度。理解插槽的底层实现(父组件内容如何被收集、归类,再由子组件在渲染阶段以渲染函数的形式执行)有助于写出更稳定、更可维护的组件。
随着 Vue 的演进,插槽实现从 "VNode 数组 + 渲染函数同时存在" 的混合表示,走向了 Vue 3 更统一的函数化表示,这带来了更好的编译优化、类型支持与运行时性能。
本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。