Lazy loaded image
前端
Vue组件通信新思路:巧用Provide/Inject与Ref优化兄弟组件交互
字数 1779阅读时长 5 分钟
2024-4-19
2025-5-12
type
status
date
slug
summary
tags
category
icon
password
在日常Vue.js开发中,我们经常会遇到这样的场景:一个页面包含多个区域,例如头部区域(Header)和中间内容区域(Content)是兄弟组件,而头部区域的一个按钮需要修改中间内容区域的某些数据或状态。
notion image

常规解决方案

面对这种兄弟组件通信的需求,我们通常会采用 props 搭配 emit、事件总线(如 mitt),或者父组件通过 ref 获取子组件实例来操作数据等方式。下面我们逐一回顾。

1. props + emit

这是最基础也是最常用的父子组件通信方式,通过状态提升到共同的父组件来实现兄弟组件间的通信。
父组件 (Parent Component): 将共享状态(如 title)定义在父组件中,并通过 props 传给子组件,同时监听子组件 emit 的事件来更新状态。
notion image
Header 组件: 通过 emit 将事件发送给父组件,请求修改 title
notion image
Content 组件: 通过 props 接收来自父组件的 title
notion image
思路总结:
  1. 将共享变量 title 提升到父组件。
  1. Header 组件通过 emit 将修改事件通知父组件,父组件负责修改 title 的值,变更会自动同步到 Content 组件。

2. emit + ref

另一种方式是,Header 组件点击按钮时 emit 事件到父组件,父组件利用 ref 获取 Content 组件的实例,然后直接调用 Content 组件内部的方法或修改其暴露的参数来更新内容。

3. mitt (事件总线)

使用 mitt 这样的事件总线库,可以实现任意组件间的通信。一个组件通过 mitt.emit('eventName', data) 发布事件,另一个组件通过 mitt.on('eventName', handler) 订阅并处理该事件,从而实现数据修改。
以上这些方案都是开发中比较常见的,各有优劣。但接下来,我要介绍一种我同事提出的、思路更为“清奇”的方案。

我同事的“黑科技”:Provide + Inject + Ref 实例

重点来了!我同事采用的是 provide + inject + ref 实例的组合方式来实现兄弟组件间的直接通信。
父组件 (Parent Component): 在父组件中,通过 provide 提供一个包含所有子组件 ref 实例的对象。
notion image
Header 组件: 通过 inject 注入父组件提供的 provideRefs 对象,从而可以直接访问到 Content 组件的 ref 实例。
notion image
Content 组件: 同样通过 inject 注入 provideRefs,并使用 defineExpose 暴露出需要被其他组件访问的属性或方法。
notion image
核心思路:
  1. 利用 provide 在父组件中收集并提供所有子组件的 ref 实例。
  1. 利用 inject 将这个包含所有子组件 ref 实例的对象注入到各个子组件中。
  1. 这样,任何一个子组件都可以通过注入的 provideRefs 对象,直接获取到其他兄弟组件的 ref 实例,进而修改其参数或调用其内部方法。
  1. 关键点: 被操作的子组件(如 Content 组件)必须使用 defineExpose 明确暴露出希望被外部访问的参数或方法。
看完这个方案,我直呼:
notion image

方案对比与思考

这种 provide/inject 结合 ref 的思路确实比较新颖。在组件层级较多、结构复杂,尤其是涉及跨多层级或大量兄弟组件通信时,传统的 props/emit 可能会导致大量的事件传递和状态提升,代码显得冗余。

场景一:调用子组件方法

假设 Header 组件的按钮需要调用 Content 组件的刷新表格方法 (fetchTable)。 传统方式:
  1. 在父组件中定义一个 fetchTableFlag 变量,通过 props 传递给 Content 组件。
  1. Header 组件点击按钮时,emit 事件到父组件,父组件修改 fetchTableFlag 的值。
  1. Content 组件通过 watch 监听 fetchTableFlag 的变化,一旦变化就调用刷新表格的方法。
这样的交互如果频繁出现,会导致父组件充斥着各种中间状态变量,代码量随之增加。而采用我同事的方法,Header 组件可以直接通过注入的 ref 调用 Content 组件暴露的 fetchTable 方法,确实减少了中间环节和代码量。

场景二:多组件复杂交互

如果页面中不仅仅是 Header 和 Content 这两个兄弟组件,而是有五六个甚至更多的兄弟组件,或者存在爷孙组件间的通信需求。此时,你可能会考虑使用状态管理库如 Pinia。 然而,如果这些组件间的交互并非共享大量全局状态,仅仅是某个组件偶尔需要改变另一个组件的某个局部状态或触发某个行为,那么将这些零散的状态都放入 Pinia 可能会显得“过重”。
我同事的这种 provide/inject + ref 的方式,在某种程度上将组件实例的访问“扁平化”了,使得兄弟组件之间可以直接“对话”,而无需通过父组件层层传递或依赖全局状态管理器。
总结与权衡: 这种方案为组件通信提供了一种新的视角,尤其适用于那些“通信不算高频,但传统方式又嫌麻烦”的场景。它简化了某些特定场景下的代码,减少了不必要的 props 和 emits。
然而,也需要注意:
  • 可维护性与可读性: 过度使用可能导致组件间的依赖关系变得不那么直观和清晰,因为一个组件可以直接修改另一个组件的内部状态。这可能需要团队有良好的约定和文档。
  • defineExpose 的必要性: 必须在子组件中显式暴露接口,否则无法从外部访问。
  • 适用场景: 更适合内部封装、耦合度较高的组件集,或者在明确知道不会轻易变动结构的小型项目中。对于大型、需要高度解耦的项目,传统的 props/emit 或状态管理库可能依然是更稳妥的选择。
总而言之,没有银弹。选择哪种通信方案,最终还是取决于项目的具体需求、团队的开发习惯以及对代码可维护性的考量。这种方法,无疑为我们的工具箱增添了一个有趣且在特定情况下非常有效的选项。
 
上一篇
掌握Python字符串前缀:b, r, u, f 的含义与妙用
下一篇
Vue3实战:轻松实现PDF预览与打印功能 (后台管理必备)