玩转Vue插槽:从基础到高级应用场景(内含为何Vue 2 不支持多根节点)

前言

  • 常网IT源码上线啦!
  • 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
  • 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。

每个人在满足了基本的生理和安全需求之后,都会渴望"尊重需求"和"自我实现需求"。

当你把对方当成英雄时,你恰恰满足了他这种深层次的心理渴望。

他会觉得,和你聊天很舒服。

一、前言

在Vue组件开发中,插槽(Slot)是实现组件复用和内容分发的核心机制。

直入正文。

插槽Slot。

二、默认插槽与具名插槽

name="footer":具名插槽,带有名字

java 复制代码
<!-- 子组件 -->
<template>
  <div>
    <slot name="header"></slot>
    <slot>默认内容</slot>
    <slot name="footer"></slot>
  </div>
</template>

<!-- 父组件 -->
<ChildComponent>
  <template v-slot:header>
    <h1>自定义标题</h1>
  </template>
  
  <p>主要内容区域</p>
  
  <template v-slot:footer>
    <footer>页脚信息</footer>
  </template>
</ChildComponent>

条件插槽:智能内容显示

<slot v-if="$slots.group" name="group"></slot>

检测插槽内容是否存在,避免渲染空内容。

三、作用域插槽:子传父数据流

数据传递机制

java 复制代码
<!-- 子组件 -->
<slot name="group" :group="groupData"></slot>

<!-- 父组件 -->
<template v-slot:group="slotProps">
  {{ slotProps.group.name }} ({{ slotProps.group.members }}人)
</template>

比如我们最常用的动态表格渲染。

java 复制代码
<!-- Table组件 -->
<template>
  <table>
    <tr v-for="(item, index) in items" :key="index">
      <slot name="row" :item="item"></slot>
    </tr>
  </table>
</template>

<!-- 使用 -->
<Table :items="users">
  <template v-slot:row="{ item }">
    <td>{{ item.name }}</td>
    <td>{{ item.email }}</td>
    <td>{{ item.role }}</td>
  </template>
</Table>

四、事件传递:父组件触发子组件事件

子组件

java 复制代码
<slot 
  name="group" 
  :group="group" 
  :on-click="handleGroupClick">  <!-- 注意:传递函数引用而非调用 -->
</slot>

<script>
export default {
  methods: {
    handleGroupClick(group) {
      console.log('分组被点击', group)
      this.$set(group, 'isExpanded', !group.isExpanded)
    }
  }
}
</script>

父组件

java 复制代码
<template v-slot:group="slotProps">
  <div @click="slotProps.onClick(slotProps.group)">
    {{ slotProps.group.name }}
    <Icon :type="slotProps.group.isExpanded ? 'up' : 'down'" />
  </div>
</template>

避免在插槽prop中使用onClick()形式,这会导致立即执行。

可折叠表单组

实战一下,子组件实现。

java 复制代码
<template>
  <div v-for="(group, index) in groups" :key="index">
    <div class="group-header">
      <slot v-if="$slots.group" 
            name="group" 
            :group="group"
            :toggle="() => toggleGroup(group)">
      </slot>
      <div v-else @click="toggleGroup(group)">
        {{ group.name }}
      </div>
    </div>

    <transition name="fade">
      <div v-show="group.isExpanded">
        <!-- 表单内容 -->
      </div>
    </transition>
  </div>
</template>

<script>
export default {
  methods: {
    toggleGroup(group) {
      this.$set(group, 'isExpanded', !group.isExpanded)
    }
  }
}
</script>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s, max-height 0.3s;
  max-height: 1000px;
  overflow: hidden;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
  max-height: 0;
}
</style>

父组件使用

java 复制代码
<CollapsibleForm :groups="formGroups">
  <template v-slot:group="{ group, toggle }">
    <div class="group-header">
      <h3>{{ group.name }}</h3>
      <button @click="toggle">
        {{ group.isExpanded ? '收起' : '展开' }}
        <Icon :name="group.isExpanded ? 'collapse' : 'expand'" />
      </button>
    </div>
  </template>
</CollapsibleForm>
  • 条件插槽避免不必要的内容渲染

  • 作用域插槽减少props传递层级

五、动态插槽名

实现原理​​:

  • Vue 3 使用 ES6 的 [ ] 计算属性语法实现动态插槽名

  • 编译阶段将动态插槽名转换为渲染函数参数

  • 运行时通过 resolveDynamicComponent 函数解析实际插槽

java 复制代码
<!-- 父组件 -->
<template>
  <DynamicComponent>
    <template v-slot:[currentSlot]>
      当前显示: {{ currentSlot }} 的内容
    </template>
  </DynamicComponent>
</template>

<script setup>
import { ref } from 'vue'
const currentSlot = ref('header') // 可动态切换为 'footer' 或 'content'
</script>
  • 实现运行时内容切换

  • 支持响应式数据驱动

  • 适用于国际化、权限控制等场景

多语言内容切换用起来就舒服了。

java 复制代码
<LocalizedContent>
  <template v-slot:[currentLang]>
    {{ messages[currentLang] }}
  </template>
</LocalizedContent>

<script setup>
const currentLang = ref('zh-CN')
const messages = {
  'zh-CN': '你好世界',
  'en-US': 'Hello World',
  'ja-JP': 'こんにちは世界'
}
</script>

多根节点

既然讲到vue3,顺便说说多根节点。

为什么 Vue 2 无法支持多根节点?

是有什么难题吗还是什么?

Vue 2 的虚拟 DOM 实现基于​​单根树结构​​,每个组件必须返回一个单一的根 VNode(虚拟节点)。这种设计简化了:

  • ​Diff 算法​​:通过递归比较单树结构,实现高效的 DOM 更新

  • ​生命周期管理​​:组件的挂载/卸载操作有明确的入口点

  • ​属性继承​​:父组件传递的属性可以明确绑定到根元素

Vue 2 的模板编译器将模板转换为渲染函数时,要求模板必须有​​单一根元素​​:

java 复制代码
<!-- 有效模板 -->
<div>
  <h1>标题</h1>
  <p>内容</p>
</div>

<!-- 无效模板(Vue 2) -->
<h1>标题</h1>
<p>内容</p>

编译器会抛出错误:"Component template should contain exactly one root element"

父组件传递的非 prop 属性(如 class、style、事件监听器)会自动绑定到子组件的根元素:

多根节点场景下,这种自动继承机制无法确定应该应用到哪个元素。

$el 属性指向组件实例的根 DOM 元素:

java 复制代码
mounted() {
  console.log(this.$el) // 根DOM元素
}

多根节点情况下,$el 应该指向哪个元素?这个问题没有明确的解决方案。

Vue 3 如何突破多根节点限制?

之前vue2是因为框架设计就是如此,想要支持多根节点,得重构框架了。

Vue 3 引入了特殊的 ​​Fragment 节点​​,作为多根组件的逻辑容器:

java 复制代码
// 编译后的渲染函数
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(),
    _createBlock(Fragment, null, [
      _createVNode("h1", null, "标题"),
      _createVNode("p", null, "内容")
    ])
  )
}

Fragment 节点特点:

  • 不渲染为实际 DOM 元素

  • 在虚拟 DOM 中作为逻辑容器

  • 不影响实际 DOM 结构

Vue 3 重写了虚拟 DOM 的 diff 算法(称为 "patch" 算法),使其能处理 Fragment 节点:

  • 支持同级多节点比较

  • 使用 Map 进行 key 索引优化

  • 支持片段移动检测

默认不会自动继承属性,需要显式绑定

java 复制代码
<template>
  <header v-bind="$attrs">标题区域</header>
  <main>主要内容</main>
  <footer>底部信息</footer>
</template>

<script>
export default {
  inheritAttrs: false // 禁用自动继承
}
</script>

属性继承规则​​:

  1. 所有非 prop 属性存储在 $attrs 对象中

  2. 需要手动指定哪些元素继承属性

  3. 事件监听器存储在 $attrs.onClick 等形式中

插槽与多根节点的结合实践

java 复制代码
<!-- LayoutSystem.vue -->
<template>
  <div v-if="$slots.top" class="top-section">
    <slot name="top" />
  </div>
  
  <div class="main-content">
    <slot />
  </div>
  
  <div v-if="$slots.aside" class="sidebar">
    <slot name="aside" />
  </div>
</template>

<!-- 使用 -->
<LayoutSystem>
  <template #top>
    <NavigationBar />
  </template>
  
  <ArticleContent />
  
  <template #aside>
    <RecommendationPanel />
  </template>
</LayoutSystem>
  • 消除不必要的包裹元素

  • 更自然的语义化模板

  • 灵活控制属性继承

不像vue2,还要外层用一个div元素包裹起来。

动态门户组件

动态插槽名 + 多根节点的一种实践。

PortalComponent.vue

java 复制代码
<template>
  <component :is="containerElement" v-for="(slot, index) in activeSlots" :key="index">
    <slot :name="slot" />
  </component>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
  slots: Array,
  containerElement: {
    type: String,
    default: 'div'
  }
})

const activeSlots = computed(() => 
  props.slots.filter(slot => useSlots()[slot])
)
</script>

使用

java 复制代码
<PortalComponent :slots="['header', 'notification', 'footer']">
  <template #header>...</template>
  <template #notification>...</template>
</PortalComponent>

六、Vue 2 vs Vue 3 插槽差异

特性 Vue 2 Vue 3
作用域插槽 slot-scope v-slot 统一语法
默认插槽 slot v-slot:default
动态插槽名 不支持 v-slot:[dynamicName]
$slots API this.$slots useSlots() 组合式API
碎片支持 单根节点限制 支持多根节点

一些个人的建议:

  1. 命名规范​ ​:使用kebab-case命名插槽(如action-buttons

  2. ​作用域控制​​:仅暴露必要的数据和方法

  3. ​性能优化​ ​:对动态内容使用v-if="$slots.name"

  4. ​文档注释​​:清晰说明插槽的预期结构和可用属性

至此撒花~

后记

插槽可以让Vue组件具备了极强的灵活性和可扩展性。可以构建出既高度复用又充分定制的组件体系,大幅提升开发效率和代码质量。

我们在实际项目中或多或少遇到一些奇奇怪怪的问题。

自己也会对一些写法的思考,为什么不行🤔,又为什么行了?

最后,祝君能拿下满意的offer。

我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

以往推荐

vue2和Vue3和React的diff算法展开说说:从原理到优化策略

前端哪有什么设计模式(14k+)

小小导出,我大前端足矣!

前端仔,快把dist部署到Nginx上

多图详解,一次性啃懂原型链(上万字)

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

VuePress搭建项目组件文档

原文链接

juejin.cn/post/751158...

相关推荐
PAK向日葵1 小时前
【算法导论】PDD 0817笔试题题解
算法·面试
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
excel5 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼5 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy5 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
草梅友仁5 小时前
草梅 Auth 1.4.0 发布与 ESLint v9 更新 | 2025 年第 33 周草梅周报
vue.js·github·nuxt.js
ZXT5 小时前
promise & async await总结
前端