深入解析Vue2插槽

插槽是组件模板中的一个占位符,父组件可以向这个占位符中插入任何模板代码,包括 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("可以插入多个元素")])
    ]
  )
}

这里需要说明一下:

  1. _c 是创建 VNode(虚拟节点) 的函数
  2. _t 是渲染槽(renderSlot)的缩写

运行时处理流程

初始化阶段:

  1. Vue 将父组件中的子组件标签中的元素收集到 vm.$slots.default数组中
  2. 如果没有提供内容,就会使用标签内的后被内容

渲染阶段:

  1. 子组件遇到 _t("default")
  2. $slots.default数组中获取VNode(虚拟节点)数组
  3. 将这些 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>

描述:

  1. 在子组件中使用 name 属性定义插槽名
  2. 在父组件中使用 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 必须处理可能复杂的动态结构

运行时处理流程

初始化阶段:

  1. vue 将父组件的模板内容编译为虚拟节点
  2. 解析所有<template v-slot:xxx></template>和带有 slot属性的内容
  3. 按照插槽名称分类存储到vm.$slots

渲染阶段:

  1. 子组件遇到<slot name="xxx">
  2. 通过 renderSlot 函数从$slot 中查找对应名称的 VNode
  3. 将匹配的 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. 作用域插槽

有时让插槽内容能够访问``子组件中才有的数据是很有用的。

假设现在我们想要使用插槽来替换掉现在插槽的备用内容,我们又该如何实现?

作用域插槽可以实现让父组件的子组件标签中使用子组件的数据

使用步骤:

  1. 在子组件的插槽中动态绑定自定义属性来将子组件的数据分发
  2. 然后在父组件中使用 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

在这篇文章中,说到了默认插槽、具名插槽、作用域插槽,以及一些插槽的原理。

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax