当前位置: 首页
前端开发
Vue2中非父子组件通信与动态组件的实现方法

Vue2中非父子组件通信与动态组件的实现方法

热心网友 时间:2026-07-09
转载

Vue2 跨层级通信与组件进阶:EventBus、全局组件与动态组件

在 Vue2 开发中,父子组件通信用 `props` + `$emit` 基本能解决大部分问题,但一旦遇到兄弟组件、跨多层组件,或者完全不相干的组件之间要传递消息,这套基础方案就不太够用了。本篇要聊的正是这类场景的解决方案——状态提升、EventBus、全局事件总线、动态组件和全局组件,这些技术是 Vuex 出现之前最常用的跨组件通信手段,也是理解 Vuex 设计思路的前置知识。 先梳理一下核心概念和适用场景,后面再展开具体代码。 **核心名词速查** | 名词 | 一句话定义 | |------|-----------| | 状态提升 | 将共享状态移至最近公共祖先,由祖先统一管理 | | EventBus | 利用 Vue 实例的 emit/on 实现发布-订阅的通信中间层 | | 全局事件总线 $bus | 挂载在 Vue.prototype 上的公共 EventBus 实例 | | .native 修饰符 | 让原生 DOM 事件监听器绑定到组件的根元素而非自定义事件系统 | | 全局组件 | 通过 Vue.component 注册,在任意实例/组件中无需再次声明即可使用 | | 动态组件 | `` 根据变量名在运行时切换渲染的组件 | **为什么要学这些** 父子通信虽然好用,但面对兄弟组件、跨多层级、完全无关联的组件时就会力不从心。这篇介绍的方案是 Vuex 出现之前的最常用手段,也是理解 Vuex 设计动机的前置知识。同时,全局组件和动态组件是构建复用性高的 UI 框架的基础工具。 **跨层级通信方案全景** 动手写代码之前,先建立一张“按场景选方案”的全景图。同样是“非父子通信”,组件树深浅、是否需要被 DevTools 追踪、是否固定层级,决定了应该选哪条路径。 这张图把四种方案钉到各自的适用场景上。黄色“难题”是所有方案的共同起点——兄弟、跨多层、无关联组件没有直接父子关系;蓝色“状态提升”把数据上提到公共祖先,靠 props/emit 流转,只适合浅层树;绿色“EventBus / 全局 bus”用独立 Vue 实例做发布-订阅中转站,能跨任意层级解耦;紫色“provide/inject”适合“祖先一次注入、后代按需取用”的组件库/插件场景;橙色“Vuex”是集中式状态仓库,唯一能被 Vue DevTools 时间旅行追踪的方案,留给大型项目。为什么不无脑上 Vuex?因为它引入样板代码与心智负担,小项目用 `$bus` 三行就够了——工具选型的本质是匹配复杂度,而不是追求“最强”。本篇覆盖前三类(状态提升、EventBus/$bus、provide/inject),Vuex 留待后续专题。 --- ### 状态提升:最纯粹的跨组件通信 先说说最基础也是最纯粹的一种方案:状态提升。 **概念与底层原理** 所谓状态提升,就是把共享的数据“向上提”到最近的公共父组件,由父组件统一持有并分发。这个概念来自 React 社区,但在 Vue2 中同样适用。 状态提升本质上是单向数据流的延伸:数据从上往下(props)流,事件从下往上($emit)冒泡。父组件既是“数据仓库”又是“消息中转站”。 Vue 官方对单向数据流有明确定义:“所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。”并且“每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告”。 状态提升正是把这条规则用到极致:兄弟组件各自没有直接通道,唯有把共享数据上提到公共祖先,才能让它们读到“同一份单向下行的数据源”。需要留意一个底层陷阱:Ja vaScript 中对象和数组按引用传递,即便是单向下行,子组件若直接 `push`/改属性操作 prop 数组本身,仍会“穿透”影响父级状态——这也是为什么示例里删除任务要走 `$emit('update:task-list', 过滤后的新数组)` 返回一个新数组,而非就地 `splice` 原数组。 这张图是状态提升的数据流闭环。紫色父组件独占 `taskList` 这一份“事实来源”;三个子组件分工——蓝色 Header 负责输入、绿色 Main 负责展示、黄色 Footer 负责统计。注意箭头方向:向下的三条是 `props`(数据 + 回调函数下传),向上的两条是事件上报(Header 调用父传入的函数、Main 通过 `$emit('update:task-list')`)。为什么数据非要放在父组件而不是各子组件各存一份?因为兄弟组件之间没有直接通道,只有“上提到公共祖先”才能让它们读到同一份数据——这正是 Vue 单向数据流“数据下行、事件上行”原则的直接体现。 **完整示例:TodoList 任务管理** 下面是一个将 `taskList` 提升到根 Vue 实例的 TodoList,Header 负责添加,Main 负责展示与删除,两者通过父级共享数据: ```html 状态提升 - TodoList

``` **要点说明** - `addTask` 以 props 函数形式传给 Header,Header 调用后将新标题传回父级,父级负责生成完整任务对象。这是“父级掌控数据,子级只负责触发”的典型写法。 - `.sync` 是 `:task-list="taskList" @update:task-list="taskList=$event"` 的语法糖,Main 通过 `$emit('update:task-list', newList)` 请求父级更新。 - Footer 只读使用 `taskList`,通过计算属性统计完成数。 **实战要点** 状态提升适合组件层级不深、共享数据结构简单的场景。一旦需要跨越 3 层以上,逐层传递的 props “链条”会让代码难以维护,此时应考虑 EventBus 或 Vuex。 不要在子组件内直接修改 props,Vue 会发出警告且逻辑难以追踪。始终通过 $emit 或回调函数通知父级修改。 **本章小结** 状态提升是 Vue2 非父子通信的最基础方案。原理清晰、无副作用,代价是随着组件层级加深,数据传递路径变长,出现“props 穿透”问题。 | 维度 | 状态提升的表现 | |------|---------------| | 数据存放 | 共享数据上提到最近公共祖先,单一事实来源 | | 数据下行 | 父组件用 `props` 把数据/回调函数传给各子组件 | | 事件上行 | 子组件用 `$emit` 或调用父传回调请求父级修改 | | 适用层级 | 浅层树(1~2 层);超过 3 层会 props 穿透 | | 优点 | 数据流可追踪、可被 DevTools 观察、无隐式依赖 | | 缺点 | 层级深时逐层透传冗长、中间组件被迫“过路转发” | 记忆口诀:“数据上提到祖先,下行 props 上行 emit;浅树清晰深树累,超过三层换方案”。 --- ### EventBus 事件总线 当组件层级较深,或者完全不相关的组件需要通信时,状态提升就变得笨重了。这时候 EventBus 就派上了用场。 **概念与底层原理** EventBus 本质上是一个充当“消息中转站”的 Vue 实例,任何组件都可以向它发布($emit)消息,也可以向它订阅($on)消息,从而实现无直接关联组件之间的通信。这是发布-订阅模式在 Vue 中的具体应用。 Vue 实例内置了一套事件系统,核心 API 包括: | 方法 | 作用 | |------|------| | `$emit(event, ...args)` | 发布消息,触发该实例上名为 event 的所有订阅者 | | `$on(event, callback)` | 订阅消息,每次触发都执行 callback | | `$once(event, callback)` | 订阅一次,触发后自动移除 | | `$off(event, callback?)` | 取消订阅,不传 callback 则移除该事件所有订阅 | 关键约束:`$emit` 和 `$on` 必须作用在同一个 Vue 实例上才能互通。这就是为什么需要专门创建一个独立的 Vue 实例作为“总线”,而不能在不同组件各自的 `this` 上调用。 这张图按时间顺序展开一次 EventBus 通信。绿色第 1 步:购物车徽章组件 B 在 `mounted` 阶段调用 `bus.$on('cart:add', callback)`,回调被存进总线实例的 `_events` 对象;蓝色第 2 步:用户点击商品卡片 A 的“加入购物车”,A 调用 `bus.$emit('cart:add', payload)`,总线按事件名取出回调数组;橙色第 3 步:总线遍历数组执行每个回调并传入 `payload`,徽章数量随之更新。关键在于 A 和 B 互不知晓对方存在,只共享中间这个 `bus` 实例——这就是发布-订阅“发布者不知道谁订阅、订阅者不知道谁发布”的解耦本质。 **完整示例:购物车跨组件通知** ```html EventBus - 购物车通知

``` **要点说明** - `bus` 是一个独立的 Vue 实例,不挂载到 DOM,只用来持有事件系统。 - `Na vBar` 在 `mounted` 中调用 `bus.$on('cart:add', ...)` 订阅消息。注意用箭头函数使 `this` 指向 Na vBar 实例本身。 - `ProductList` 调用 `bus.$emit('cart:add', payload)` 发布消息,payload 携带商品数据。 - `beforeDestroy` 中调用 `bus.$off('cart:add')` 是防止内存泄漏的关键步骤——不取消订阅,组件销毁后监听函数仍然存在于内存中。 **关于内存泄漏的两种常见写法** ```ja vascript // 方式一:在 beforeDestroy 钩子中取消订阅(推荐) mounted() { bus.$on('notify', this.handleNotify); }, beforeDestroy() { bus.$off('notify', this.handleNotify); } // 方式二:使用 $once,只监听一次,自动移除 mounted() { bus.$once('user:login', this.handleLogin); } ``` 这两段是 EventBus 防内存泄漏的“标准姿势”。方式一把订阅与解绑配对:`mounted` 里 `$on(事件, this.handleNotify)`,`beforeDestroy` 里用同一个函数引用 `$off(事件, this.handleNotify)` 精准摘除——注意必须是命名方法而非匿名箭头函数,否则 `$off` 拿不到引用、删不掉。方式二用 `$once` 让总线在触发一次后自动 `$off`,适合“只处理一次”的信号。 **源码级机制:$on/$emit 到底做了什么** 很多人把 EventBus 当“黑盒”用,但理解它的源码实现能彻底消除“为什么必须同一个实例”“为什么大小写敏感”这类困惑。Vue2 的四个事件方法由 `eventsMixin` 一次性挂到 `Vue.prototype` 上,它们的本质就是一份手写的发布-订阅实现,核心数据结构是每个实例上的私有属性 `_events`。 这张图拆解事件中心 `_events` 的全生命周期。蓝色 initEvents:实例初始化时执行 `vm._events = Object.create(null)`,创建一个无原型的纯净空对象当事件中心;绿色 $on:核心一行 `(_events[event] || (_events[event]=[])).push(fn)`,把回调推进以事件名为 key 的数组;黄色数据结构:最终 `_events` 长成 `{ 'cart:add': [fn1, fn2], 'user:login': [fn3] }` 这种“事件名 → 回调数组”的字典;橙色 $emit:取出 `_events[event]` 数组、用 `slice` 截掉第一个参数得到附加参数、遍历 `cb.apply(this, args)` 逐个执行;红色 $off:从对应数组里 `splice` 删掉指定回调。这张图回答了两个高频疑问:① 为什么 `$emit/$on` 必须同实例——因为 `_events` 是实例私有的,A 实例的 emit 只查 A 自己的 `_events`,跨实例查不到;② 为什么事件名大小写敏感——因为事件名是对象的 key,`cart:Add` 和 `cart:add` 是两个不同的 key。 **本章小结** EventBus 利用 Vue 实例自带的发布-订阅系统,将不相关的组件解耦。核心是“发布者不知道谁在监听,订阅者不知道谁在发布”,底层是每个实例私有的 `_events` 字典。 | API | 作用 | 何时调用 | 关键陷阱 | |-----|------|----------|---------| | `$on(事件, fn)` | 订阅消息,每次触发都执行 | `mounted` 中订阅 | 必须配对 `$off`,否则内存泄漏 | | `$emit(事件, payload)` | 发布消息并传参 | 动作触发时 | 必须与订阅在同一实例 | | `$once(事件, fn)` | 只订阅一次,触发后自动解绑 | 只处理一次的信号 | 无需手动 `$off` | | `$off(事件, fn)` | 取消订阅 | `beforeDestroy` 中 | 不传 fn 会移除该事件所有订阅 | 记忆口诀:“独立实例当总线,on 订阅 emit 发;同实例才能通,destroy 别忘 off”。 --- ### 全局事件总线 $bus EventBus 虽然好用,但每次都要手动 import 总线实例略显麻烦。全局事件总线 $bus 把这个痛点解决了。 **概念与底层原理** 全局事件总线是将一个 Vue 实例挂载到 `Vue.prototype.$bus`,使得所有 Vue 实例和组件都能通过 `this.$bus` 访问同一个总线对象,无需手动传递 `bus` 引用。 Vue 实例与组件实例之间存在原型链继承关系。一个组件实例的原型链是:组件实例 → VueComponent.prototype → Vue.prototype → Object.prototype。当我们执行 `Vue.prototype.$bus = new Vue()`,相当于把总线挂在原型链上游的 Vue.prototype 上;任何组件读 `this.$bus` 时,JS 引擎沿 `__proto__` 逐级向上查找,在 `Vue.prototype` 上命中,于是全应用共享同一个 `$bus` 实例。这就是“零配置全局访问”的本质——靠的是 JS 原型链的属性查找机制,而非 Vue 的什么魔法。 这也解释了为什么 `Vue.prototype.$bus = new Vue()` 必须在第一个 `new Vue()` 之前执行:实例创建时会确定原型链,挂载晚了,早创建的实例链上还没有 `$bus`。 ```ja vascript // 必须在 new Vue() 之前执行,确保所有组件都能访问 Vue.prototype.$bus = new Vue(); ``` 这一行是全局事件总线的“安装语句”,本质是往 `Vue.prototype` 上挂一个共享的 Vue 实例当总线。实战中通常写在入口文件 `main.js` 顶部、`new Vue({...}).$mount('#app')` 之前;更规范的做法是封装成插件 `Vue.use({ install(Vue){ Vue.prototype.$bus = new Vue() } })`,把“挂载时机”收敛到插件安装阶段统一管理。 **完整示例:全局消息通知栏** 下面演示一个横跨多层组件的消息通知系统,任意组件都可以发出通知,通知栏始终在顶层: ```html 全局事件总线 $bus

用户操作面板

``` **要点说明** - `Vue.prototype.$bus = new Vue()` 在任何 Vue 实例创建之前执行,确保所有组件通过 `this.$bus` 访问同一个总线。 - 通知栏在 `mounted` 中订阅 `notify` 事件,接收 `{ msg, type, duration }` 格式的载荷。 - 任意组件(无论在组件树的哪一层)调用 `this.$bus.$emit('notify', payload)` 都能触发通知栏更新,无需感知通知栏的存在。 **实战要点** - 挂载时机:`Vue.prototype.$bus = new Vue()` 必须在第一个 `new Vue()` 之前执行。 - 命名规范:事件名建议采用 `模块:动作` 的格式(如 `cart:add`、`user:logout`),避免不同业务模块的事件名冲突。 - 与独立 EventBus 的区别:独立 EventBus 需要手动 `import bus from './bus'`,`$bus` 则通过原型链自动注入,更简便,但在大型项目中隐式依赖更难追踪。 **本章小结** `$bus` 是对 EventBus 模式的工程化封装,借助 Vue 原型链注入实现零配置全局访问。适合中小型项目的跨组件通信,大型项目建议迁移至 Vuex 以获得 DevTools 的状态追踪支持。 | 维度 | 独立 EventBus(`import bus`) | 全局 `$bus`(`Vue.prototype.$bus`) | |------|-------------------------------|--------------------------------------| | 获取方式 | 每个文件手动 `import bus from './bus'` | `this.$bus` 原型链自动注入 | | 实现原理 | 模块单例,靠 ES Module 缓存共享 | 挂在 `Vue.prototype`,靠原型链查找 | | 挂载时机 | 任意,import 即用 | 必须在第一个 `new Vue()` 之前 | | 依赖可见性 | 显式 import,依赖清晰可追踪 | 隐式注入,大型项目较难追踪来源 | | 适用规模 | 任意,模块化项目首选 | 中小项目、快速开发首选 | 记忆口诀:“原型挂总线,组件链上找;new Vue 前先挂,全局 this 点 bus 用”。 --- ### 为组件添加原生事件:.native 修饰符 在父组件使用子组件时,有时需要监听组件根元素的原生 DOM 事件,比如点击遮罩关闭弹窗。这时候 .native 修饰符就派上了用场。 **概念与底层原理** 当你在组件上写 `@click="fn"` 时,Vue 会将其理解为“监听子组件 `$emit('click')` 事件”,而非 DOM 的 click 事件。这意味着子组件如果不显式调用 `this.$emit('click')`,父组件的监听器永远不会触发。 `.native` 修饰符改变了这一行为:它将事件监听器绑定到组件根元素的真实 DOM 上。 Vue 官方对 `.native` 修饰符的定义是:“` .native` —— 监听组件根元素的原生事件”。这句话点明了它的边界——监听的是“组件根元素”,而非组件内部任意 DOM;若组件根元素不是你想监听的那个节点,`.native` 也会绑到根元素上而非你期望的子节点。 ```ja vascript // 等价于 document.querySelector(组件根元素).addEventListener('click', fn); ``` 这行伪代码点出 `.native` 的底层语义:它不走 Vue 的自定义事件系统,而是直接在组件根元素的真实 DOM 上调用 `addEventListener('click', fn)`。编译层面,Vue 把带 `.native` 的监听器收集进虚拟节点 data 的 `nativeOn` 字段(普通自定义事件放 `on` 字段),patch 阶段 `nativeOn` 里的监听器被绑定到根元素 DOM 上。 左红右绿对比两条事件路径。红色“不加 .native”:父组件写 `@click="fn"` 被 Vue 理解成“监听子组件 `$emit('click')` 的自定义事件”,可子组件内部从不 `$emit('click')`,于是 `fn` 永不触发——这是初学者最常踩的“点了没反应”坑。绿色“加 .native”:`@click.native="fn"` 让监听器绕过自定义事件系统、直接挂到组件根元素的真实 DOM 上,用户点击根元素即触发 `fn`。一句话区分:没有 `.native`,`@click` 等的是子组件“主动喊一嗓子”;加了 `.native`,等的是“用户真实点到那块 DOM”。注意 Vue3 已移除 `.native`,改为未被 `emits` 声明的监听器自动透传到根元素。 **完整示例:弹窗关闭按钮** ```html .native 修饰符

操作结果:{{ result }}

``` **要点说明** - `ConfirmModal` 的根元素是 `.modal-overlay` 遮罩层,组件内部从不 `$emit('click')`。 - 父组件写 `@click.native="handleOverlayClick"`,将 click 监听器绑定到遮罩层的真实 DOM 上,实现“点击遮罩关闭弹窗”。 - 弹窗内容区 `.modal` 上写了 `@click.stop`,阻止 click 事件冒泡到遮罩层,避免点击弹窗内部时意外触发关闭逻辑。 **实战要点** - `.native` 只适用于自定义组件,普通 HTML 标签上使用 `.native` 无效。 - Vue3 已移除 `.native` 修饰符,改为在组件的 `emits` 选项中声明所有自定义事件,未声明的 `@xxx` 监听器自动穿透到根元素。 **本章小结** `.native` 修饰符解决了“父组件想监听组件根元素的原生 DOM 事件,但子组件没有 $emit 对应事件”的问题。 | 维度 | `@click`(无 .native) | `@click.native` | |------|----------------------|-----------------| | 监听对象 | 子组件 `$emit('click')` 的自定义事件 | 子组件根元素的原生 DOM click | | 触发条件 | 子组件主动 `$emit('click')` 才触发 | 用户真实点击根元素 DOM 即触发 | | 编译归宿 | vnode data 的 `on` 字段 | vnode data 的 `nativeOn` 字段 | | 适用对象 | 自定义组件 | 仅自定义组件(普通标签上无效) | | Vue3 状态 | 保留 | 已移除,改用 `emits` + 自动透传 | 记忆口诀:“无 native 等组件喊(emit),有 native 听真实点;只对组件才有用,Vue3 改成自动穿”。 --- ### 全局组件注册与使用 前面聊的都是组件之间的通信,接下来看看组件本身的注册机制。 **概念与底层原理** 全局组件通过 `Vue.component(name, options)` 注册,一次注册后在任意 Vue 实例、任意局部组件的模板中均可直接使用,无需在 `components` 选项中重复声明。 `Vue.component` 将组件配置对象注册到 Vue 构造函数的全局组件表(`Vue.options.components`)。所有通过 `new Vue()` 创建的实例在解析模板时,先查找局部 `components`,若未找到则回溯到全局组件表。 Vue 官方对二者的定位描述为:全局注册的组件“可以用在其被注册之后的任何(通过 `new Vue`)新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中”,而局部注册的组件“只有在该类型的组件才可以访问”。 这张图展示模板编译时“组件名 → 组件定义”的解析顺序。蓝色:模板解析遇到 `` 标签;黄色:先查当前实例自己的 `components` 选项(局部组件表);紫色:局部没找到才回溯到 `Vue.options.components`(全局表);绿色:任一层命中即拿到定义渲染;红色:两层都找不到,控制台抛 `Unknown custom element` 警告。这个“局部优先、全局兜底”的查找链解释了两件事:① 全局组件无需在每个 `components` 里重复声明就能用——它在回溯的最后一层兜底;② 局部同名组件会覆盖全局组件,因为局部表先被命中。 ```ja vascript // 必须在 new Vue() 之前注册 Vue.component('组件名', { data() { return { ... } }, props: [...], template: `...` }); ``` 这是全局注册的标准签名,`options` 与局部组件配置格式完全一致。它把组件挂进 `Vue.options.components`,因此必须在 `new Vue()` 之前调用。 **完整示例:通用数字徽章组件** ```html 全局组件注册

首页 消息 购物车 通知

``` **要点说明** - `Vue.component('v-badge', { ... })` 将徽章组件注册到全局,之后所有模板都能直接写 `` 标签。 - `UserPanel` 是局部组件,内部使用了 ``,无需在自己 `components` 中声明。 - 组件命名建议使用带连字符的小写格式,与 HTML 原生元素区分。 **实战要点** - 注册时机:`Vue.component` 必须在 `new Vue()` 之前调用。 - 适用组件:适合高频复用的基础 UI 组件(按钮、输入框、徽章、图标等)。业务逻辑组件建议使用局部注册,避免全局污染。 - 在 Vue CLI 项目中,通常将全局组件注册逻辑放在 `main.js` 中,或使用 `require.context` 批量自动注册。 **本章小结** 全局组件是构建 UI 组件库(Element UI、Ant Design Vue 等)的基础机制。`Vue.use(ElementUI)` 的本质就是批量调用 `Vue.component` 注册所有基础组件。 | 维度 | 全局组件(`Vue.component`) | 局部组件(`components` 选项) | |------|----------------------------|----------------------------| | 注册位置 | `Vue.options.components` 全局表 | 当前实例的 `vm.$options.components` | | 使用范围 | 注册后任意实例/组件模板直接用 | 仅注册它的那个组件内部可用 | | 是否重复声明 | 一次注册,处处可用 | 每个用到的组件都要单独声明 | | 打包影响 | 即使没用到也会被打包(无法 tree-shaking) | 按需引入,利于代码分割 | | 适用场景 | 高频通用基础 UI(按钮、徽章、图标) | 页面级业务组件,需隔离 | | 注册时机 | 必须在 `new Vue()` 之前 | 随组件定义,无时机限制 | 记忆口诀:“全局一次处处用,局部声明各自管;通用 UI 走全局,业务组件用局部”。 --- ### 动态组件:component :is 最后来看看动态组件,它让“运行时按需渲染哪个组件”这件事变得非常简单。 **概念与底层原理** 动态组件是 Vue 内置的 `` 元素配合 `:is` 属性,根据变量的值在运行时动态决定渲染哪个组件,无需写大量 `v-if/v-else-if`。 `:is` 的值可以是字符串(组件注册名称)或组件配置对象。 ```ja vascript // 以下两种写法等价: // 动态组件写法 // 手写 v-if 链等价写法(当组件多时非常冗余) ``` 这两段对比凸显动态组件的价值:上方一行 ` ` 把“渲染哪个组件”交给运行时的 `currentTab` 变量,组件越多越简洁;下方的 `v-if/v-else-if` 链每加一个组件就要多写一行判断。底层上 `` 是 Vue 内置的“占位组件”,渲染时根据 `:is` 的值动态生成对应 vnode。 `keep-alive` 是 Vue 内置组件,包裹动态组件后,被切换走的组件不会销毁,而是缓存在内存中,再次切换回来时直接恢复,不重新执行 `created/mounted`。 **不加 keep-alive 时的生命周期行为**:每次切换组件,旧组件执行 `beforeDestroy` + `destroyed`,新组件执行 `created` + `mounted`。 **加 keep-alive 后的生命周期行为**:被切换走的组件执行 `deactivated`(停用),再次切换回来执行 `activated`(激活),跳过 `created`、`mounted`、`beforeDestroy`。 **完整示例:Tab 标签页内容切换** ```html 动态组件 component :is

动态组件与 keep-alive

生命周期日志(验证 keep-alive 效果):

{{ log.time }} [{{ log.component }}] {{ log.event }}

``` **要点说明** - `` 根据 `currentTab` 字符串值动态渲染对应名称的组件,切换时只改变 `currentTab` 的值即可。 - `` 包裹后,切走的组件不执行 `beforeDestroy`,而是执行 `deactivated`;切回来时不执行 `created/mounted`,而是执行 `activated`。计数器的值得以保留。 - 若去掉 ``,每次切换组件都会销毁旧组件、创建新组件,计数器每次归零。 **实战要点** - `keep-alive` 适合切换频繁、组件内部有状态或有昂贵渲染逻辑的场景。 - `keep-alive` 支持 `include`/`exclude` 属性,精确控制缓存哪些组件:``。 - 不要无条件对所有组件使用 `keep-alive`,缓存过多组件会占用大量内存。 **本章小结** 动态组件将“渲染哪个组件”的决策从静态模板移到了运行时数据,配合 `keep-alive` 实现有状态的 Tab 切换、步骤向导等 UI 模式。 | 维度 | 不加 `keep-alive` | 加 `keep-alive` | |------|------------------|-----------------| | 切走时钩子 | `beforeDestroy → destroyed`(销毁) | `deactivated`(进入缓存,不销毁) | | 切回时钩子 | `created → mounted`(重建) | `activated`(从缓存恢复,跳过 created/mounted) | | 内部状态 | 每次切换归零、重新初始化 | 保留(计数器、滚动位置、表单填写) | | 内存占用 | 低(用完即销毁) | 高(实例常驻,`max` 控制 LRU 淘汰) | | 数据刷新写在 | `mounted` | `activated`(每次进入都执行) | | 资源清理写在 | `beforeDestroy` | `deactivated` | 记忆口诀:“is 按名换组件,v-if 长链全省去;keep-alive 缓状态,activated 进 deactivated 出”。 --- ### 实例引用通信:$refs / $parent / provide-inject 前面的方案要么靠数据流(状态提升)、要么靠事件流(EventBus)。Vue 还提供了第三类“直接拿到组件实例”的通信手段:`$refs`、`$parent`/`$children`、`provide`/`inject`。它们更“命令式”,是官方文档归为“处理边界情况”的逃生舱。 **名词解释** | 名词 | 一句话定义 | |------|-----------| | `$refs` | 通过 `ref` 属性给子组件/DOM 打标记,父组件用 `this.$refs.xxx` 直接拿到子组件实例或真实 DOM | | `$parent` / `$children` | 子组件用 `this.$parent` 访问父实例,父组件用 `this.$children` 访问直接子实例数组 | | `provide` / `inject` | 祖先用 `provide` 提供数据/方法,任意深度的后代用 `inject` 按 key 注入,跳过中间层 | **概念与底层原理** `$refs` 是命令式访问的逃生舱。官方明确:“`$refs` 只会在组件渲染完成之后生效,并且它们不是响应式的。这只是一个用于直接操作子组件的『逃生舱』——你应该避免在模板或计算属性中访问 `$refs`”。底层上,编译器把 `ref="foo"` 编译进 vnode 的 `ref` 字段,patch 阶段把对应实例/DOM 注册到 `vm.$refs.foo`——所以挂载前 `$refs` 为空,且它不参与依赖收集,改了不会触发重渲染。 `$parent` 通信耦合度高,慎用。官方警告:“在大多数情况下,触达父级组件会使你的应用更难调试和理解,尤其是当你修改了父级的数据的时候。” `provide/inject` 解决“props 穿透”,但默认不响应。官方强调:“`provide` 和 `inject` 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。”这意味着 `provide: { color: this.color }` 传基本类型时后代拿到的是快照、祖先改了不更新;要响应必须 `provide: { theme: this.themeObj }` 传一个响应式对象,后代读它的属性才会响应。 ```ja vascript // $refs:父组件命令式调用子组件方法 / 操作 DOM this.$refs.userForm.validate(); this.$refs.videoEl.play(); // provide / inject:祖先提供响应式对象,后代按 key 注入 // 祖先组件 export default { provide() { return { theme: this.theme }; }, data() { return { theme: { color: 'blue' } }; } }; // 任意深度的后代组件 export default { inject: ['theme'], computed: { textColor() { return this.theme.color; } } }; ``` `$refs` 部分是命令式的:父组件拿到子组件实例后直接调用它暴露的 `validate()`,或拿到真实 DOM 调用 `play()`——注意只能在 `mounted` 之后、事件回调里用。`provide/inject` 部分专治“props 穿透”:祖先一次 `provide`,跨任意层级的后代直接 `inject`,中间组件无需透传——但关键细节是 `provide` 要传对象而非基本类型。 **三种方案的选型边界** | 方案 | 通信方向 | 是否响应 | 适用场景 | 主要风险 | |------|---------|:-------:|----------|---------| | `$refs` | 父 → 子(命令式) | 否 | 调子组件方法、操作 DOM、聚焦/播放 | 渲染后才有值、滥用导致命令式耦合 | | `$parent`/`$children` | 子 ↔ 父 | 是(实例数据) | 紧耦合的组合组件内部 | 强依赖父级结构,复用性差 | | `provide`/`inject` | 祖先 → 后代 | 默认否(传对象可响应) | 组件库跨层共享上下文、主题/配置注入 | 隐式依赖、绑定不响应易踩坑 | **实战要点** - `$refs` 拿到的是“渲染后”的引用,改数据后想立刻用 `$refs` 须放进 `this.$nextTick(() => { ... })`。 - 业务代码优先用 `props/$emit` 与 `$bus`,`$parent`/`$children` 仅在“明确是一组紧耦合组件”(如 `Tabs` + `TabPane`)时使用。 - `provide/inject` 要响应式,记住“提供一个对象,注入读它的属性”;或在 Vue2.6+ 配合 `Vue.observable()` 创建响应式数据源再 provide。 **本章小结** `$refs`/`$parent`/`provide-inject` 是 Vue 在“数据流 + 事件流”之外补充的“实例引用”通信。它们更直接但更易耦合:`$refs` 做命令式调用、`$parent` 做紧耦合组合、`provide/inject` 做跨层依赖注入。记住一句话——能用 props/$emit/bus 解决的,就别用实例引用;非用不可时,把耦合控制在“明确成组”的组件内部。 --- ### 总结 **知识点回顾** 这张图把全篇五大主题串成一条知识脉络。蓝色状态提升——数据上提公共祖先、props/emit 流转,适合浅层树;绿色 EventBus/$bus——独立 Vue 实例的 `_events` 事件中心,`emit/on/once/off` 四件套,`Vue.prototype.$bus` 原型链注入,`beforeDestroy` 必须 `off` 防泄漏;黄色实例引用通信——`$refs` 命令式访问、`$parent/$children` 紧耦合组合、`provide/inject` 跨层注入;橙色 .native 修饰符——绑定根元素原生事件、Vue3 已移除;红色组件机制——`Vue.component` 全局注册、`component :is` 动态切换、`keep-alive` 缓存配合 `activated/deactivated`。复习时建议沿“数据流 → 事件流 → 实例引用 → 组件机制”四条主线回溯。 **高频面试题速查** | 问题 | 要点 | |------|------| | Vue2 非父子通信有哪些方案? | 状态提升、EventBus/$bus、provide/inject、Vuex | | EventBus 为什么要在 beforeDestroy 中 $off? | 不取消订阅,组件销毁后监听函数仍存内存,造成内存泄漏 | | $bus 为什么所有组件都能访问? | 挂在 Vue.prototype 上,所有实例通过原型链查找可访问 | | @click 和 @click.native 区别? | 前者监听 $emit 自定义事件,后者绑定根元素原生 DOM 事件 | | 全局组件和局部组件如何选择? | 全局适合高频通用 UI 组件,局部适合业务功能组件 | | keep-alive 引入了哪些钩子? | activated(从缓存恢复)和 deactivated(进入缓存) | | :is 的值可以是什么类型? | 字符串(组件名)或组件配置对象 | **学习建议** 1. **动手验证生命周期**:用第六章的示例,分别加上和去掉 ``,对比日志观察生命周期差异,是理解 keep-alive 最直接的方式。 2. **从 EventBus 迁移到 Vuex**:当项目中 EventBus 的事件超过 10 个,或者事件触发链路超过 3 跳时,这是引入 Vuex 的信号。 3. **理解原型链注入**:`$bus`、`$router`、`$store` 都是通过 `Vue.prototype` 注入的,掌握这一原理后,看任何 Vue 插件的源码都会轻松很多。 4. **Vue3 的变化对照**:EventBus 在 Vue3 中被官方移除,需要用第三方库(如 `mitt`)替代。`.native` 修饰符也被移除,`keep-alive` 的 `include/exclude` 配合 `defineComponent` 的 `name` 属性使用,提前了解这些变化有助于平滑升级。
来源:https://juejin.cn/post/7655870187364548654

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
如何用拖放在Flask看板应用中更新数据库状态

如何用拖放在Flask看板应用中更新数据库状态

在Flask看板应用中,通过HTML5拖放API结合前端fetch异步POST请求,将拖拽任务的task_id和target_state发送至后端路由;后端接收JSON后更新数据库并提交事务,实现跨列拖拽时实时更新状态。需注意将事件绑定到容器而非子元素,并在ondragover中阻止默认行为以确保drop触发。

时间:2026-07-09 07:01
如何用CSS :has()选择器快速实现悬停span显示相邻div

如何用CSS :has()选择器快速实现悬停span显示相邻div

利用CSS的:has()伪类选择器,通过` bar:has(> baz:hover)+ qux`实现悬停内联元素时显示相邻块级容器,解决了传统相邻兄弟选择器无法跨层级匹配的局限,可用于悬停图标显示详情框等交互场景。需注意浏览器兼容性和性能优化,避免过度使用影响渲染效率。

时间:2026-07-09 07:01
display:block居中段落文本的响应式实现方法

display:block居中段落文本的响应式实现方法

通过`display:block`与`max-width`限制宽度,配合`margin:auto`使段落块水平居中并实现响应式布局;同时用CSS统一控制段落间距,避免脱离文档流,提升可读性与维护性。

时间:2026-07-09 07:01
JavaScript代码去重:高效避免重复编写的技巧

JavaScript代码去重:高效避免重复编写的技巧

将重复代码中的变化点提取为参数,利用ES6模板字符串动态生成选择器,可消除重复、保持一致性。注意参数安全性,必要时进行白名单校验;批量化场景可扩展为数组形式;现代框架中更推荐数据驱动或组件化封装替代手动选择器。

时间:2026-07-09 07:00
使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

利用CSS容器查询单位cqw,表单输入框宽度可直接基于祖父容器计算,绕过中间包装器尺寸干扰,实现简洁响应式布局,无需硬编码或JavaScript,显著降低维护成本,提升嵌套结构下的布局灵活性。

时间:2026-07-09 07:00
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜