深入解析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

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

相关推荐
苏格拉没有底了35 分钟前
由频繁创建3D火焰造成的内存泄漏问题
前端
阿彬爱学习37 分钟前
大模型在垂直场景的创新应用:搜索、推荐、营销与客服新玩法
前端·javascript·easyui
橙序员小站1 小时前
通过trae开发你的第一个Chrome扩展插件
前端·javascript·后端
Lazy_zheng1 小时前
一文掌握:JavaScript 数组常用方法的手写实现
前端·javascript·面试
是晓晓吖1 小时前
关于Chrome Extension option的一些小事
前端·chrome
MrSkye1 小时前
🔥从菜鸟到高手:彻底搞懂 JavaScript 事件循环只需这一篇(下)
前端·javascript·面试
方佑1 小时前
✨ Nuxt 混合渲染实践: MemOS前端体验深度优化指南
前端
爱编程的喵1 小时前
React 19 + Vite 6 构建现代化旅行应用智旅(1)
前端·react.js
l1t1 小时前
使用流式函数解决v语言zstd程序解压缩失败问题
前端·压缩·v语言·zstd
小离a_a1 小时前
el-tree方法的整理
前端·vue.js·elementui