一文通透 Vue动态组件体系:插槽|数据监听|组件通信|动态切换|缓存—闭环

0 阅读14分钟

疏通Vue动态组件体系:插槽、数据监听、组件通信、动态组件与缓存,完整知识闭环

不知道大家有没有这种感觉,学 Vue 的时候知识点总是东一块西一块。 插槽单独学、监听单独记、组件通信挨个背,代码调用会写,但脑子一团乱麻。 只懂怎么用API,完全搞不懂每个知识点在整个框架体系里处在什么位置、互相有什么联系。

我觉得学习不能只停留在会敲代码,更要理清底层逻辑、打通知识脉络,搭建属于自己的认知体系。 写这篇文章,更多是学习梳理、复盘感悟,把整条组件化完整思路串通透。

本文会顺着最简单的逻辑,由浅入深、从内到外,完整串联整套动态体系: 结构动态 → 数据动态 → 组件数据互通 → 组件整体切换 → 组件状态缓存 全程通俗易懂、逻辑闭环,读完彻底搞懂Vue组件动态底层思想。

一、为什么我们需要动态组件

最朴素直白地理解: 写死固定不变的页面,就是静态组件。 页面长啥样,打开就永远啥样,结构不动、数据不动、内容不动,呆呆板板,僵硬得不行。

动态,顾名思义就是页面会变化、内容会刷新、视图会跟着数据自动改动。 用户点击、数据更新、状态切换、内容联动,页面可以灵活做出响应,这就是动态。

所以动态能力,是Vue组件开发的灵魂所在。 Vue设计插槽、数据监听、组件通信、组件切换一系列API,归根结底,都是为了一件事: 让组件灵活可变,让页面活起来。

二、结构动态:插槽 Slot,灵活自定义组件DOM

想要组件不再死板,最先要解决的就是布局结构固化的问题,插槽就是 Vue 用来实现结构分发的核心方案

简单理解: 插槽就是在子组件中预留空位,允许父组件自由传入任意DOM结构,灵活改变子组件内部布局。

Vue 一共提供三类插槽,覆盖绝大多数开发场景:

- 默认插槽:基础内容分发

- 具名插槽:多区域精准布局

- 作用域插槽:子组件存数据,父组件自定义渲染结构

下面简单学习了解一下

一、默认插槽

作用:实现父子组件之间 HTML DOM 结构传递子组件预留占位位置,父组件可传入任意标签内容

Vue2 代码示范

👉 子组件 Child.vue

<template>
  <div class="card-box">
    <h4>我是子组件内部固定标题</h4>
    <!--
      默认插槽
      作用:预留一个空白位置
      用来接收父组件传递过来的任意DOM结构
    -->
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "Child"
}
</script>

  👉 父组件 Parent.vue

<template>
  <div class="parent">
    <h3>父组件页面</h3>
    <!--
      子组件标签内部所有内容
      都会被分发到子组件 <slot> 位置渲染
    -->
    <Child>
      <p>我是父组件传入的段落内容</p >
      <button>父组件自定义按钮</button>
    </Child>
  </div>
</template>

<script>
import Child from './Child.vue'
export default {
  components: { Child }
}
</script>

Vue3 代码示范:默认插槽 Vue2 和 Vue3 语法完全一致,无需改动

 

二、具名插槽

作用一个组件多个渲染区域通过插槽名字,精准分发不同位置的DOM结构 多用于页面布局:头部、侧边、主体、底部

Vue2 代码示范

👉 子组件 Child.vue

<template>
  <div class="layout">
    <!-- 头部插槽,命名 header -->
    <slot name="header"></slot>

    <!-- 主体内容插槽,命名 main -->
    <slot name="main"></slot>

    <!-- 底部插槽,命名 footer -->
    <slot name="footer"></slot>
  </div>
</template>

<script>
export default {
  name: "Child"
}
</script>

👉 父组件 Parent.vue

<template>
  <div>
    <Child>
      <!-- slot="名称" 匹配子组件对应插槽 -->
      <div slot="header"> 页面头部区域</div>
      <div slot="main"> 页面主体内容</div>
      <div slot="footer"> 页面底部信息</div>
    </Child>
  </div>
</template>

<script>
import Child from './Child.vue'
export default {
  components: { Child }
}
</script>

Vue3 代码示范

👉 子组件 Child.vue写法不变

👉 父组件 Parent.vue

Vue3 彻底废弃 slot="" 行内写法,统一使用  v-slot:名称 ,简写  #名称 ,必须包裹 template

<template>
  <div>
    <Child>
      <!-- # 是 v-slot: 的简写语法 -->
      <template #header>
        <div>Vue3 专属头部</div>
      </template>

      <template #main>
        <div>Vue3 主体内容区域</div>
      </template>

      <template #footer>
        <div>Vue3 底部</div>
      </template>
    </Child>
  </div>
</template>

<script setup>
import Child from './Child.vue'
</script>

三、作用域插槽(重点)

核心逻辑

数据存放在子组件,DOM结构由父组件自定义编写 子组件向外暴露自己的数据 父组件拿到数据,自由决定标签样式 (业务场景:表格单元格、列表自定义渲染)

Vue2 代码示范

👉 子组件 Child.vue

<template>
  <div class="list-box">
    <!--
      作用域插槽
      :listData 向外抛出子组件内部数据
      把数据传递给父组件使用
    -->
    <slot :listData="userList"></slot>
  </div>
</template>

<script>
export default {
  name: "Child",
  data() {
    return {
      // 数据完全由子组件维护
      userList: [
        { id: 1, name: "张三" },
        { id: 2, name: "李四" },
        { id: 3, name: "王五" }
      ]
    }
  }
}
</script>

👉 父组件 Parent.vue

<template>
  <div>
    <!--
      slot-scope 用来接收子组件传递过来的所有数据
      scope 是自定义接收对象
    -->
    <Child slot-scope="scope">
      <!-- 从scope中取出子组件的数据,自定义渲染结构 -->
      <div>用户姓名:{{ scope.listData.name }}</div>
    </Child>
  </div>
</template>

<script>
import Child from './Child.vue'
export default {
  components: { Child }
}
</script>

Vue3 代码示范

👉 子组件 Child.vue

<template>
  <div class="list-box">
    <!-- 向外暴露子组件内部数据 -->
    <slot :listData="userList"></slot>
  </div>
</template>

<script setup>
// 子组件自身数据
const userList = [
  { id: 1, name: "张三" },
  { id: 2, name: "李四" },
  { id: 3, name: "王五" }
]
</script>

👉 父组件 Parent.vue

Vue3 删除 slot-scope,全部统一插槽语法

<template>
  <div>
    <!-- #default 代表默认作用域插槽,接收子组件数据 -->
    <Child #default="scope">
      <!-- 父组件自由编写DOM,使用子组件数据 -->
      <div style="color:red">
        自定义用户:{{ scope.listData.name }}
      </div>
    </Child>
  </div>
</template>

<script setup>
import Child from './Child.vue'
</script>

对比一下 Vue2 vs Vue3 插槽差异

1. 默认插槽 Vue2、Vue3 语法完全一致,无任何区别

2. 具名插槽

  • Vue2:直接  slot="名字"  写在标签上
  • Vue3:必须使用  #名字 ,外层包裹  template ,不再支持行内slot

3. 作用域插槽

  • Vue2:专用关键字  slot-scope="变量" 
  • Vue3:全部统一为  v-slot / #  语法,大一统

插槽本质上,只改变组件内部DOM,组件本身不会发生变化,属于组件内部结构层面的动态。

三、数据动态:computed 计算属性 & watch 侦听器

解决完结构问题,我们需要让组件内部的数据拥有响应变化的能力,这里就离不开 computedwatch

一、computed 计算属性

依赖已有数据自动生成全新数据,具备缓存特性,被动触发执行,适合数据拼接、数值换算、状态判断等简单数据处理,只支持同步代码。

1. 基本用法代码(Vue3)
<script setup>
import { computed, ref } from 'vue'

// 原始响应式数据
const num1 = ref(10)
const num2 = ref(20)

// 计算属性:依赖现有数据,自动算出新值
const total = computed(() => {
  console.log('计算属性执行了')
  // 依赖 num1 和 num2
  return num1.value + num2.value
})
</script>
2. 主动性 VS 被动性

- computed 是被动触发 :你不去读取它,它永远不执行 只有页面用到、代码读取 total 的时候,它才会计算

3. 依赖关系:多对一

多个原始数据 → 一个计算属性  num1、num2  多个变量,共同生成 一个 total

4. 自带缓存(最核心特性)

只要它依赖的数据没有发生变化,无论你读取多少次 computed,函数只执行一次,直接读缓存,性能极好

5. 只能同步,不能写异步

computed 内部严禁异步请求、定时器 一旦写异步,依赖收集直接失效,整个废掉

6. 本质

数据派生器 根据已有数据,自动推导新数据 属于:数据 → 数据

二、watch 侦听器

主动监听数据变化,数据一旦改变就立刻执行回调函数,无缓存机制,天然支持异步业务逻辑。 日常开发中还有两个高频配置:

-  immediate :页面首次加载立即执行监听

-  deep:开启深度监听,能够监听到对象、数组内部属性变化

1. 用法代码(含 deep、immediate)
<script setup>
import { watch, ref } from 'vue'

const count = ref(0)

// 监听 count 变化
watch(
  count,
  (newVal, oldVal) => {
    // 数据一变,立刻进入这里
    console.log('数据变化了', newVal)
  },
  {
    immediate: true, // 页面一加载立刻执行一次
    deep: true // 深度监听对象、数组内部变化
  }
)
</script>
2. 主动性 VS 被动性

- watch 是主动监听 只要我监听的数据发生改变 不管你用不用、读不读 自动立刻触发函数

3. 依赖关系:一对多

一个被监听数据 可以触发 一大堆业务逻辑、请求、操作、修改其他变量

一个数据变动 → 触发无数行为

4. 无缓存

数据变一次,执行一次 变多少次,跑多少次 不存在缓存

5. 天生支持异步

watch 里面随便写: 接口请求、定时器、复杂判断、大量业务代码 完全没问题

6. 本质

数据变化监视器 盯着一个值,变了就做事 属于:数据变化 → 行为动作

简单区分:需要加工数据用 computed,数据变化要做业务操作用 watch。

二者搭配使用,让组件数据可以自动计算、实时监听、随时更新,真正实现数据动态响应。

四、数据互通:Vue 四大组件通信方案

插槽控制结构、监听控制数据,但每个组件都是独立作用域,数据相互隔离无法共享。 想要多个组件联动变化,就必须掌握全套组件通信方式。

四种通信清晰划分为两大层级,方便理解与选用:

第一层级:基础点对点通信

1.  props + $emit  — 父子直系通信

2.  provide + inject  — 祖孙跨层通信

第二层级:全局架构级通信

3.  EventBus  事件总线 — 无关组件轻量通信

4.  Pinia  全局状态仓库 — 大型项目统一状态管理

第一层级:基础点对点组件通信详解

特点:组件与组件直接一对一、一对多传值 语法简单、使用频率最高、代码完整、细节拉满

1. 父子组件通信 props + $emit

Vue 最正统、最基础、使用最多的父子通信方式

1.1 父向子传值 — props

抽象概念

  • 数据流向:单向自上而下 父组件 → 子组件
  • 主动被动关系:父组件主动推送数据,子组件被动接收数据
  • 数据映射关系:一对多 一个父组件,可以同时给多个子组件传递同一份数据
  • 数据流特性:单向数据流 数据源头在父组件,子组件只能读取,不允许直接修改 props 数据
  • 使用范围:仅限直接父子嵌套组件

代码示例

👉 父组件(数据发送方)

<template>
  <!-- 通过自定义属性,把数据传递给子组件 -->
  <Child :msg="parentMsg" />
</template>

<script setup>
// 引入子组件
import Child from './Child.vue'

// 父组件内部定义响应式数据
const parentMsg = "我是来自父组件的传递数据"
</script>

  👉 子组件(数据接收方)

<template>
  <!-- 直接使用父组件传递过来的数据 -->
  <div>接收父组件数据:{{ msg }}</div>
</template>

<script setup>
// 显性声明需要接收父组件哪些参数
const props = defineProps(['msg'])
</script>
1.2 子向父传值 — $emit 自定义事件

抽象概念

  • 数据流向:自下而上 子组件 → 父组件
  • 主动被动关系:子组件主动触发事件,父组件被动监听、接收数据
  • 数据映射关系:一对多 一个子组件触发事件,可以被多个上层父组件监听
  • 底层逻辑:子组件自定义事件,触发事件时携带自身数据向上抛出

代码示例

👉 子组件(数据发送方)

<template>
  <!-- 点击触发方法,向父组件发送数据 -->
  <button @click="sendChildData">把数据传给父组件</button>
</template>

<script setup>
// 定义当前组件需要向外派发的自定义事件
const emit = defineEmits(['getChildInfo'])

// 子组件自身私有数据
const childInfo = "这里是子组件内部数据"

// 触发事件,携带数据向上传递
const sendChildData = () => {
  // 参数1:事件名称  参数2:要传递的数据
  emit('getChildInfo', childInfo)
}
</script>

👉 父组件(数据接收方)

<template>
  <!-- 监听子组件抛出的自定义事件,触发对应回调函数 -->
  <Child @getChildInfo="handleGetData" />
</template>

<script setup>
// 回调函数,接收子组件传递过来的所有数据
const handleGetData = (value) => {
  console.log('成功接收子组件数据:', value)
}
</script>

父子通信总结

- props:属性下发,父传子,负责数据流入 - $emit:事件上抛,子传父,负责数据反馈

一上一下、单向流动、结构规范、日常开发最常用

2. 隔代祖孙通信 provide + inject

抽象底层概念

  • 解决痛点:多层嵌套组件,如果用 props 需要一层一层往下传递,中间组件无辜转发、代码冗余
  • 数据流向:顶层祖先组件 → 所有下层后代组件
  • 主动被动:上层主动提供数据,下层所有后代被动注入获取
  • 映射关系:一对多 一个祖先组件,任意层级的孙子、曾孙子都可以直接拿到数据
  • 核心能力:组件层级穿透,无视中间嵌套层数

👉 顶层祖先组件(提供数据)

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

// 向外穿透提供数据,所有后代组件均可访问
provide('theme', '全局暗色主题')
</script>

👉 任意深层后代组件(孙子、重孙子)

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

// 直接注入顶层数据,不用管中间嵌套多少层组件
const theme = inject('theme')
</script>

第二层级:全局架构级通信(弱化代码,侧重思想、场景、定位)

不属于简单两个组件点对点传值,偏向项目整体数据流架构,这里简单讲解,不堆砌大量代码

3. 无关组件通信 EventBus 事件总线

抽象概念

1. 适用场景:两个组件不存在任何父子、祖孙嵌套关系,互相独立

2. 底层原理:发布订阅设计模式

  • 发布方:主动发射事件、携带数据
  • 订阅方:监听对应事件,被动接收数据

3. 数据关系:多对多通信

通俗理解

相当于项目里一个公共中转站组件A把数据丢进总线,组件B、C、D监听总线就能拿到数据

(使用场景小型项目、简单兄弟组件临时通信 缺点:事件杂乱难管理,大型项目基本淘汰)

4. 全局状态管理 Pinia

抽象本质

前面所有通信,都是组件和组件之间互相传数据 Pinia 直接改变思路:所有组件统一读写公共数据仓库 (可以看前面的文章有讲解,这里一笔带过)

到这里我们可以总结:

插槽、数据监听、组件通信,全部都是在组件内部做变化。 组件不会被替换,只是结构、数据、内容在动态流转更新。

五、更高维度动态:component :is 整体动态组件

component :is  本身用法十分简单,几乎所有接触过 Vue 项目的人都不陌生。 很多人日常业务中一直在使用,只是对动态组件这个专业名词不够熟悉。

它不是新增语法,也不是复杂API,是 Vue 框架原生自带、从诞生之初就存在的能力。 放在我们整套组件动态体系里看,它有着非常清晰的层级定位:

  • 插槽:负责组件内部结构动态
  • 父子通信:负责组件内部数据动态
  • component 动态组件:负责组件整体层面的动态切换

前面所有知识点,都在优化单个组件内部。 而动态组件,上升到了组件与组件之间的灵活渲染。

六、动态组件优化:keep-alive 组件缓存

我们用 component :is 动态切换组件。 默认情况下:组件一切换,旧组件直接销毁,新组件重新创建。

只要组件离开视线:

  • 组件  onUnmounted  卸载销毁
  • 里面填写的数据、输入框内容、页面状态全部清空
  • 下次切回来,重新执行  onMounted  重新请求、重新初始化

很多业务场景我们并不希望组件被销毁 比如表单填写、搜索列表、浏览页面、标签页切换。

于是 Vue 提供了内置缓存组件:keep-alive

1. 先进行定位和了解

  • 插槽:组件内部结构动态
  • 组件通信:组件数据动态
  • component:is:组件整体动态切换
  • keep-alive:组件切换不销毁、状态保留、生命周期缓存

不写 keep-alive

组件切换:  onMounted  挂载 → 切换 →  onUnmounted  销毁 每次进出都完整创建+销毁

加上 keep-alive

组件不会走挂载、销毁 多出两个专属生命周期钩子:

  •  onActivated 组件被激活、显示
  •  onDeactivated 组件休眠、隐藏

简单大白话: 组件只是藏起来,不是删掉 数据、输入内容、页面状态全部保留。

它不是用来写页面的,专门控制组件生命周期。

2.简单代码示例

直接包裹我们的动态组件即可加上切换按钮完整可运行代码

<template>
  <button @click="currentCom = 'Home'">首页</button>
  <button @click="currentCom = 'User'">用户</button>

  <!-- 缓存组件,切换不销毁 -->
  <keep-alive>
    <component :is="currentCom"></component>
  </keep-alive>
</template>

<script setup>
import { ref } from 'vue'
import Home from '@/components/Home.vue'
import User from '@/components/User.vue'

const currentCom = ref('Home')
</script>

3. keep-alive (两个重要属性)

1. include 只缓存指定组件

<!-- 只缓存 Home 和 User -->
<keep-alive include="Home,User">
  <component :is="currentCom"></component>
</keep-alive>

2. exclude 唯独不缓存某个组件

<!-- 除了 Cart 全都缓存 -->
<keep-alive exclude="Cart">
  <component :is="currentCom"></component>
</keep-alive>

七、全文梳理总结

从头到尾,就围绕一件事来梳理,就是:怎么让 Vue 组件不再死板固定,一步步变得灵活、动态、好用。

最开始我们认识了插槽 slot,它只负责在组件内部动手脚,让一个组件里面的结构、标签内容可以自由自定义,不用把组件写死。

之后学习了父子组件传值,解决了组件之间数据互通的问题,让组件内部的数据也能流动变化。

紧接着我们了解了  component + is  动态组件。 这个东西大家平时写项目天天用,只是专业名词可能见得少。Vue 很早就自带了。作用也很直白:不再固定渲染某一个组件标签,通过变量直接切换项目里不同的 vue 文件,实现一整个组件整体替换。

最后登场的 keep-alive,可以说是动态组件的最佳搭档。 组件一切换默认就会销毁重建,页面数据、填写内容全部清空。而 keep-alive 专门用来缓存组件状态,让组件只是隐藏休眠,不会真正销毁,既保留页面数据,又优化页面性能。搭配  include 、 exclude  还能精准控制哪些组件需要缓存、哪些不需要。

整体梳理一条完整链路:

插槽 → 改变组件内部结构

组件通信 → 流转组件内部数据

动态组件 → 切换整个组件本体

keep-alive → 缓存组件生命周期与页面状态

把零散知识点梳理通顺、理清底层逻辑,简单白话分享出来,一起学习吃透 Vue 组件思想。