插槽是组件模板中的一个占位符,父组件可以向这个占位符中插入任何模板代码,包括 HTML、其他的组件等
简单的说就是,子组件中给父组件提供了一个占位符,用<slot></slot>
表示,父组件可以在子组件标签之间填充任何模板代码,添加的代码都会被解析到子组件的占位符中。
那么接下来就深度剖析一下 vue 中的插槽。
1. 插槽的类型
1.1. 默认插槽
默认插槽允许父组件向子组件插入未命名的内容。当子组件内部存在未命名的标签时,父组件中所有未指定插槽名称的内容都会自动分发到这个位置。
1.1.1. 用法
xml
<!-- ChildComponent.vue -->
<div class="child">
<h3>子组件标题</h3>
<slot>这是后备内容(当父组件不提供内容时显示)</slot>
<p>子组件底部</p>
</div>
xml
<child-component>
<p>这是父组件插入的内容</p>
<div>可以插入多个元素</div>
</child-component>
最终渲染的结果:
xml
<div class="child">
<h3>子组件标题</h3>
<p>这是父组件插入的内容</p>
<div>可以插入多个元素</div>
<p>子组件底部</p>
</div>
1.1.2. 原理
编译阶段
javascript
with(this) {
return _c('div',
{ staticClass: "child" },
[
_c('h3', [_v("子组件标题")]),
_t("default", [_v("这是后备内容")]), // _t 表示 renderSlot
_c('p', [_v("子组件底部")])
]
)
}
javascript
with(this) {
return _c('child-component',
[
_c('p', [_v("这是父组件插入的内容")]),
_c('div', [_v("可以插入多个元素")])
]
)
}
这里需要说明一下:
- _c 是创建 VNode(虚拟节点) 的函数
- _t 是渲染槽(renderSlot)的缩写
运行时处理流程
初始化阶段:
- Vue 将父组件中的子组件标签中的元素收集到
vm.$slots.default
数组中 - 如果没有提供内容,就会使用标签内的后被内容
渲染阶段:
- 子组件遇到
_t("default")
时 - 从
$slots.default
数组中获取VNode(虚拟节点)数组 - 将这些 VNode(虚拟节点) 插入到子组件对应的位置
那么我们来看一下虚拟 DOM 结构
css
{
tag: 'child-component',
children: [
{ tag: 'p', children: ['这是父组件插入的内容'] },
{ tag: 'div', children: ['可以插入多个元素'] }
]
}
css
{
tag: 'div',
children: [
{ tag: 'h3', children: ['子组件标题'] },
{ tag: 'p', children: ['这是父组件插入的内容'] },
{ tag: 'div', children: ['可以插入多个元素'] },
{ tag: 'p', children: ['子组件底部'] }
]
}
1.1.3. 核心代码分析
php
/**
* 解析组件插槽内容,将子节点分类到对应的插槽中
* @param {Array<VNode>} children - 父组件的子节点数组(即插槽内容)
* @param {Component} context - 当前组件上下文
* @return {Object} 返回按插槽名分组的对象
*/
function resolveSlots(children, context) {
// 初始化返回的slots对象
const slots = {};
// 如果没有子节点,直接返回空对象
if (!children) return slots;
// 遍历所有子节点
for (let i = 0; i < children.length; i++) {
const child = children[i];
// 获取节点的slot属性值(具名插槽的名称)
// child.data 包含节点的各种属性信息
const name = child.data && child.data.slot;
if (name) {
// 具名插槽处理逻辑
// 如果slots中还没有该名称的数组,则初始化为空数组
slots[name] = slots[name] || [];
// 将当前子节点推入对应名称的插槽数组
slots[name].push(child);
} else {
// 默认插槽处理逻辑(没有指定slot属性的节点)
// 初始化default数组(如果不存在)
slots.default = slots.default || [];
// 将节点推入default数组
slots.default.push(child);
}
}
// 返回格式化后的slots对象,格式如:
// {
// header: [VNode, VNode...],
// default: [VNode, VNode...],
// footer: [VNode...]
// }
return slots;
}
1.2. 具名插槽
具名插槽时 Vue 插槽中用于内容精准分发的机制,它允许组件将内容插入子组件的特定命名位置,解决了默认插槽无法处理多个内容分发点的问题
1.2.1. 用法
xml
<div class="card">
<!-- 具名插槽定义 -->
<header>
<slot name="header"></slot>
</header>
<div class="content">
<!-- 默认插槽 -->
<slot></slot>
</div>
<footer>
<slot name="footer"></slot>
</footer>
</div>
xml
<child-component>
<!-- 具名插槽内容 -->
<template v-slot:header>
<h2>卡片标题</h2>
</template>
<!-- 默认插槽内容 -->
<p>这里是卡片的主要内容...</p>
<!-- 另一个具名插槽 -->
<template v-slot:footer>
<button>确定</button>
<button>取消</button>
</template>
</child-component>
xml
<div class="card">
<header>
<h2>卡片标题</h2>
</header>
<div class="content">
<p>这里是卡片的主要内容...</p>
</div>
<footer>
<button>确定</button>
<button>取消</button>
</footer>
</div>
描述:
- 在子组件中使用 name 属性定义插槽名
- 在父组件中使用 template 标签并在其上通过 v-slot 绑定上同样的插槽名
需要注意的是,必须定义 name 属性,否则会无法找到渲染槽,导致报错
同样的我们来看一下实现原理
1.2.2. 原理
编译阶段
arduino
with(this) {
return _c('div', { staticClass: "card" }, [
_c('header', [_t("header")], 2), // _t 表示 renderSlot
_c('div', { staticClass: "content" }, [_t("default")], 2),
_c('footer', [_t("footer")], 2)
])
}
less
with(this) {
return _c('child-component', [
_c('template', { slot: "header" }, [ // 具名插槽
_c('h2', [_v("卡片标题")])
]),
_c('p', [_v("这里是卡片的主要内容...")]), // 默认插槽
_c('template', { slot: "footer" }, [ // 具名插槽
_c('button', [_v("确定")]),
_c('button', [_v("取消")])
])
])
}
可能大家不是很清楚_c('header', [_t("header")], 2)
中的2表示什么?
这个数字是位掩码,表示子节点应该如何被标准化处理:
值 | 对应常量 | 含义 |
---|---|---|
0 |
NORMALIZE_NONE |
不进行特殊处理 |
1 |
NORMALIZE_SIMPLE |
简单标准化(合并相邻文本节点) |
2 |
NORMALIZE_FORCE |
强制完全标准化(处理嵌套数组等复杂结构) |
在这里设置为 2,是为了强制标准化子节点,因为插槽内容可能是动态的、嵌套的复杂结构,必须得确保无论插槽返回什么内容(数组、片段、文本等),都能正确被渲染
场景 | 示例 | 推荐值 | 原因 |
---|---|---|---|
静态简单内容 | _c('div', ['文本']) |
0 |
无需处理已知简单的文本内容 |
动态简单内容 | _c('div', [this.message]) |
1 |
需要合并可能的文本节点 |
插槽/组件内容 | _c('div', [_t("slot")]) |
2 |
必须处理可能复杂的动态结构 |
运行时处理流程
初始化阶段:
- vue 将父组件的模板内容编译为虚拟节点
- 解析所有
<template v-slot:xxx></template>
和带有slot
属性的内容 - 按照插槽名称分类存储到
vm.$slots
中
渲染阶段:
- 子组件遇到
<slot name="xxx">
时 - 通过
renderSlot
函数从$slot
中查找对应名称的VNode
- 将匹配的
VNode
渲染到指定位置
虚拟 DOM 结构生成
css
{
tag: 'child-component',
children: [
{
tag: 'template',
data: { slot: 'header' },
children: [{ tag: 'h2', children: ['卡片标题'] }]
},
{ tag: 'p', children: ['这里是卡片的主要内容...'] },
{
tag: 'template',
data: { slot: 'footer' },
children: [
{ tag: 'button', children: ['确定'] },
{ tag: 'button', children: ['取消'] }
]
}
]
}
css
{
tag: 'div',
props: { class: 'card' },
children: [
{
tag: 'header',
children: [{ tag: 'h2', children: ['卡片标题'] }]
},
{
tag: 'div',
props: { class: 'content' },
children: [{ tag: 'p', children: ['这里是卡片的主要内容...'] }]
},
{
tag: 'footer',
children: [
{ tag: 'button', children: ['确定'] },
{ tag: 'button', children: ['取消'] }
]
}
]
}
最后说一下:具名插槽的 v-slot 也可以简写为#
例如:
arduino
<template #slotName>
...
</template>
1.3. 作用域插槽
有时让插槽内容
能够访问``子组件中
才有的数据
是很有用的。
假设现在我们想要使用插槽来替换掉现在插槽的备用内容,我们又该如何实现?
作用域插槽可以实现让父组件的子组件标签中使用子组件的数据
使用步骤:
- 在子组件的插槽中动态绑定自定义属性来将子组件的数据分发
- 然后在父组件中使用 v-slot 来绑定一个默认的接收对象,来接收子组件传递的所有数据
具体实现:
xml
<template>
<div class="child1">
<h1>子组件标题</h1>
<slot :user="user" :obj="obj">{{ user.lastName }}</slot>
<p>子组件底部</p>
</div>
</template>
<script>
export default {
data() {
return {
user: {
firstName: "张",
lastName: "三",
},
obj: {
firstName: "李四",
},
};
},
};
</script>
xml
<template>
<div id="father">
父组件:
<child-component>
<template v-slot:default="slotProps">
<p>{{ slotProps.user.firstName }}</p>
</template>
</child-component>
</div>
</template>
<script>
import childComponent from "@/components/componentInfo/childComponent.vue";
export default {
components: {
childComponent,
},
};
</script>
1.3.1. 独占默认插槽的简写语法
接下来就说一种特殊的情况,也就是说,当只有默认插槽的时候,这种简写的方式才可以使用:
sql
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>
也就是说可以直接将 v-slot 写在组件标签中,这可能还不够简洁,还可以更简洁一些:将 default 省略掉
sql
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
1.3.2. 结构插槽 prop
我们需要清楚我们在父组件中接收到的所有 prop 是被放在了一个对象中,所以是可以通过解构拿到其中的数据的:

所以我们可以:
xml
<template v-slot:default="{ user, obj }">
<p>{{ user }}</p>
<p>{{ obj }}</p>
</template>
来拿到传递来的单条数据。
为数据重命名:
xml
<template v-slot:default="{ user:person1, obj:person2 }">
<p>{{ person1 }}</p>
<p>{{ person2 }}</p>
</template>
同样的,通过重命名后的名称也可以获取到 prop 的数据
1.4. 动态插槽名
xml
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
2. 总结
接下里就来进行总结:我们先来看一下,下面的这个流程,这就是插槽的本质原理
父组件编译阶段
↓
生成包含插槽内容的渲染函数
↓
子组件实例化时解析slots
↓
子组件渲染时调用renderSlot
↓
根据插槽类型(普通/作用域)获取内容
↓
将插槽内容合并到子组件VNode
↓
生成最终DOM
在这篇文章中,说到了默认插槽、具名插槽、作用域插槽,以及一些插槽的原理。