Vue2中非父子组件通信与动态组件的实现方法
Vue2 跨层级通信与组件进阶:EventBus、全局组件与动态组件
在 Vue2 开发中,父子组件通信用 `props` + `$emit` 基本能解决大部分问题,但一旦遇到兄弟组件、跨多层组件,或者完全不相干的组件之间要传递消息,这套基础方案就不太够用了。本篇要聊的正是这类场景的解决方案——状态提升、EventBus、全局事件总线、动态组件和全局组件,这些技术是 Vuex 出现之前最常用的跨组件通信手段,也是理解 Vuex 设计思路的前置知识。 先梳理一下核心概念和适用场景,后面再展开具体代码。 **核心名词速查** | 名词 | 一句话定义 | |------|-----------| | 状态提升 | 将共享状态移至最近公共祖先,由祖先统一管理 | | EventBus | 利用 Vue 实例的 emit/on 实现发布-订阅的通信中间层 | | 全局事件总线 $bus | 挂载在 Vue.prototype 上的公共 EventBus 实例 | | .native 修饰符 | 让原生 DOM 事件监听器绑定到组件的根元素而非自定义事件系统 | | 全局组件 | 通过 Vue.component 注册,在任意实例/组件中无需再次声明即可使用 | | 动态组件 | `
这张图把四种方案钉到各自的适用场景上。黄色“难题”是所有方案的共同起点——兄弟、跨多层、无关联组件没有直接父子关系;蓝色“状态提升”把数据上提到公共祖先,靠 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
这张图按时间顺序展开一次 EventBus 通信。绿色第 1 步:购物车徽章组件 B 在 `mounted` 阶段调用 `bus.$on('cart:add', callback)`,回调被存进总线实例的 `_events` 对象;蓝色第 2 步:用户点击商品卡片 A 的“加入购物车”,A 调用 `bus.$emit('cart:add', payload)`,总线按事件名取出回调数组;橙色第 3 步:总线遍历数组执行每个回调并传入 `payload`,徽章数量随之更新。关键在于 A 和 B 互不知晓对方存在,只共享中间这个 `bus` 实例——这就是发布-订阅“发布者不知道谁订阅、订阅者不知道谁发布”的解耦本质。
**完整示例:购物车跨组件通知**
```html
这张图拆解事件中心 `_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
用户操作面板
左红右绿对比两条事件路径。红色“不加 .native”:父组件写 `@click="fn"` 被 Vue 理解成“监听子组件 `$emit('click')` 的自定义事件”,可子组件内部从不 `$emit('click')`,于是 `fn` 永不触发——这是初学者最常踩的“点了没反应”坑。绿色“加 .native”:`@click.native="fn"` 让监听器绕过自定义事件系统、直接挂到组件根元素的真实 DOM 上,用户点击根元素即触发 `fn`。一句话区分:没有 `.native`,`@click` 等的是子组件“主动喊一嗓子”;加了 `.native`,等的是“用户真实点到那块 DOM”。注意 Vue3 已移除 `.native`,改为未被 `emits` 声明的监听器自动透传到根元素。
**完整示例:弹窗关闭按钮**
```html
操作结果:{{ 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 根实例,也包括其组件树中的所有子组件的模板中”,而局部注册的组件“只有在该类型的组件才可以访问”。
这张图展示模板编译时“组件名 → 组件定义”的解析顺序。蓝色:模板解析遇到 `
首页
消息
``` **要点说明** - `Vue.component('v-badge', { ... })` 将徽章组件注册到全局,之后所有模板都能直接写 `
**完整示例:Tab 标签页内容切换**
```html
动态组件与 keep-alive
生命周期日志(验证 keep-alive 效果):
{{ log.time }} [{{ log.component }}] {{ log.event }}
``` **要点说明** - `
这张图把全篇五大主题串成一条知识脉络。蓝色状态提升——数据上提公共祖先、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. **动手验证生命周期**:用第六章的示例,分别加上和去掉 `
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
如何用拖放在Flask看板应用中更新数据库状态
在Flask看板应用中,通过HTML5拖放API结合前端fetch异步POST请求,将拖拽任务的task_id和target_state发送至后端路由;后端接收JSON后更新数据库并提交事务,实现跨列拖拽时实时更新状态。需注意将事件绑定到容器而非子元素,并在ondragover中阻止默认行为以确保drop触发。
如何用CSS :has()选择器快速实现悬停span显示相邻div
利用CSS的:has()伪类选择器,通过` bar:has(> baz:hover)+ qux`实现悬停内联元素时显示相邻块级容器,解决了传统相邻兄弟选择器无法跨层级匹配的局限,可用于悬停图标显示详情框等交互场景。需注意浏览器兼容性和性能优化,避免过度使用影响渲染效率。
display:block居中段落文本的响应式实现方法
通过`display:block`与`max-width`限制宽度,配合`margin:auto`使段落块水平居中并实现响应式布局;同时用CSS统一控制段落间距,避免脱离文档流,提升可读性与维护性。
JavaScript代码去重:高效避免重复编写的技巧
将重复代码中的变化点提取为参数,利用ES6模板字符串动态生成选择器,可消除重复、保持一致性。注意参数安全性,必要时进行白名单校验;批量化场景可扩展为数组形式;现代框架中更推荐数据驱动或组件化封装替代手动选择器。
使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度
利用CSS容器查询单位cqw,表单输入框宽度可直接基于祖父容器计算,绕过中间包装器尺寸干扰,实现简洁响应式布局,无需硬编码或JavaScript,显著降低维护成本,提升嵌套结构下的布局灵活性。
- 日榜
- 周榜
- 月榜
相关攻略
2026-07-08 06:57
2026-07-08 06:56
2026-07-08 06:56
2026-07-08 06:56
2026-07-08 06:56
2026-07-08 06:56
2026-07-08 06:56
2026-07-08 06:55
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

