玩转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...

相关推荐
顽强d石头1 分钟前
bug:undefined is not iterable (cannot read property Symbol(Symbol.iterator))
前端·bug
烛阴10 分钟前
模块/命名空间/全局类型如何共存?TS声明空间终极生存指南
前端·javascript·typescript
火车叼位14 分钟前
Git 精准移植代码:cherry-pick 简单说明
前端·git
江城开朗的豌豆18 分钟前
JavaScript篇:移动端点击的300ms魔咒:你以为用户手抖?其实是浏览器在搞事情!
前端·javascript·面试
华洛25 分钟前
聊聊我们公司的AI应用工程师每天都干啥?
前端·javascript·vue.js
江城开朗的豌豆25 分钟前
JavaScript篇:你以为事件循环都一样?浏览器和Node的差别让我栽了跟头!
前端·javascript·面试
gyx_这个杀手不太冷静28 分钟前
Vue3 响应式系统探秘:watch 如何成为你的数据侦探
前端·vue.js·架构
晴殇i34 分钟前
🌐 CDN跨域原理深度解析:浏览器安全策略的智慧设计
前端·面试·程序员
半桔44 分钟前
【算法深练】分组循环:“分”出条理,化繁为简
数据结构·c++·算法·leetcode·面试·职场和发展
Uyker1 小时前
空间利用率提升90%!小程序侧边导航设计与高级交互实现
前端·微信小程序·小程序