浅析 Vue 中的组件通信方式
组件的关系
根据组件的上下级“包含关系”,分为父子组件、祖孙组件、以及兄弟组件。
同时,根据组件关系的“紧密程度”,可以分为耦合关系和松散关系,或者说静态结构耦合与动态数据耦合。具有耦合关系的组件,其层级结构是静态的,比于:ul->li 或是 header-aside-content-foot,这类组件在使用时的结构不会变化。而具有松散关系的组件,在使用时可以是动态结构,比如下面的结构,一对括号内包含的内容可以根据需求动态选择,而非必需的:
|--table
|----((->thead)->tr->th)
|----(->tbody)->tr->td
|----((->tfoot)->tr->th)
开发时应尽量减少存在结构耦合的组件,调整为仅存在数据耦合。此时建议使用插槽代替直接将子组件嵌入父组件。 代码示例
组件间的通信方式
相关技术的分析文章已经很多了,此处不再具体展开,此处只比较各种方法的差异和可能的实用场景。相关的解析可以点击以下链接:https://blog.csdn.net/weixin_45242865/article/details/120026192
通信方式分类
根据组件间通信使用的接口类型分类,主要分为以下三种:直接访问(即不使用接口)、直接接口通信和代理接口通信。
直接访问
方法1 通过 $parent
和 $children
或者 $ref
的命令式双向通信:这种方式没有通过接口而是直接引用了目标组件。开发方便但维护属于噩梦级别。
$parent
和 $children
至少保持了组件结构,只是可读性较低;而 $ref
则是干脆去掉了结构特征,调用时无法直接识别调用方和被调用方的关系,不一定能通过调用上下文判断,还需要结合组件结构,因此不建议使用 $ref
进行组件通信。
直接接口通信
通过父组件和子组件提供的接口进行通信。组件耦合程度可能低也可能高,取决于实际开发时采取的通信策略和组件的结构耦合类型。
- 方法2 实例的自定义参数与自定义事件——通过
prop
和$emit
/v-on
的命令式双向通信:父组件通过prop
向子组件传值;子组件通过$emit
触发自身的自定义事件,然后父组件通过v-on
接收子组件自定义事件的消息。 - 方法3 实例的非参数属性和非原生事件——通过
$attr
和$listeners
的命令式单向下行通信:子(孙)组件通过这两个属性,分别获取父(祖)组件上的非prop
属性(不包括class
和style
)和非原生事件。 - 方法4 依赖注入——通过
provide
/inject
的命令式/响应式(根据提供的属性是否可监听)单向下行通信:父(祖)组件通过provide
提供数据或方法接口,子(孙)组件通过inject
使用接口来注入。 - 方法5 插槽——通过
slot
的单向命令式通信:在子组件中放置<slot>
,可以将子组件中的数据传递到父组件中,之后可以根据设计要求往插槽中嵌入合适的内容来使用子组件的数据,甚至嵌入一个组件。
代理接口通信
组件只通过代理接口进行通信,不存在直接依赖关系,耦合程度很低。
- 方法6 事件总线——通过事件总线(Bus)的命令式双向通信:与方法2 类似,但是由专门的管理器负责事件的分发,触发组件通过
$emit
触发管理器的自定义事件,接收组件通过v-on
接收管理器自定义事件的消息。 - 方法7 公共状态——通过公共状态管理的响应式双向通信:设置一个公共的状态管理器,保存不同组件共享的状态,同时,获取和设置状态只能通过状态管理器。如简单的 store 模式或是 Vuex 这类状态管理库。
通信的方向
- 单向下行通信:方法1的
$parent
,方法2的prop
,方法3、4、5均可。 - 单向上行通信:方法1的
$children
,方法2的$emit
,方法5均可。 - 双向通信:方法2、6、7均可。
- 其中,“方法5 插槽”较为特殊,其本质是提供了子组件向外的数据接口,使得外部可以操作子组件的视图,但没有规定数据传递的方向。因此既可以在父组件上通过一定操作改变子组件数据的“显示”状态,模拟上行通信的效果(注意只是模拟上行通信,因为本质上数据还是在子组件内部渲染,只是改变了渲染后的显示状态);也可以嵌入另外的组件,使子组件的数据流向这个嵌入的组件,形成下行通信。
通信方式的适用场景
对于具有不同关系的组件,可以选择以下通信策略:
- 静态结构耦合
- 结构耦合的父子组件之间:直接访问(
$parent
、$children
)或是通过接口都行,因为本身父子组件就是绑定在一起的,不一定要强行解耦。
注意,方法5、6会引入新的管理对象,因此对于简单组件不一定是最好的选择。 - 结构耦合的祖孙组件之间:建议通过接口通信而不要直接访问
- 结构耦合的兄弟组件之间:由于不存在上下级关系,可以使用代理接口通信;或者综合方法2的
prop
参数和方法5的插槽。或者,通过嵌套一个容器组件统一管理状态,再使用直接接口访问状态;但在兄弟组件很多且类型不一的情况下,反而会增加管理复杂度,此时还是建议使用代理接口。
- 结构耦合的父子组件之间:直接访问(
- 只存在动态数据耦合的动态组件之间(不论是父子组件、祖孙组件还是兄弟组件):建议使用代理接口,或者综合方法2的参数与方法5的插槽。
简单总结
- 对于兄弟组件,由于它们之间不存在直接的通信接口,也最好不要直接通信,因此都需要一个公共的代理负责。简单的组件结构可以由父组件负责状态管理;而对于复杂结构,最好通过专门的状态管理工具。
- 对于上下级组件,则需要根据组件的复杂程度和设计要求中对复用性和维护性的约束,选择合适的通信策略。
解耦组件
开发可复用的嵌套组件时,需要使各级组件保持一定程度的独立。此时建议通过插槽的方式绑定需要传入子组件的形参,并在组合各级组件时传入实参,而不是在开发组件阶段就将子组件嵌入到父组件中,并马上绑定实参。
代码示例
子组件直接嵌入父组件,形成结构耦合
<parent> <son :data="data"></son> </parent>
子组件通过插槽嵌入父组件,组件之间只存在数据耦合
// parent.vue <template> <parent> <slot name="son" :data="data"> </parent> </template> // son.vue <template> <son></son> </template> <script> ... // 省略其他内容 props: { data: { required: true: }, }, ... // 省略其他内容 <script> // container.vue <template> <parent> <!-- 插槽允许多种写法获取绑定的data, --> <!-- 但其中某些写法可能具有歧义, --> <!-- 因此只展示通过对象解构语法获取data --> <!-- 想了解更多写法可以点击下面的链接 --> <template v-slot:son="{data}"> <son :data="data"></son> </template> </parent> </template>
整个过程用函数调用类比:
将 “在第一个函数中直接调用第二个函数并传入参数” 调整为 “将第二个函数在主模块中作为回调传入第一个函数,第一个函数只在内部定义了调用回调函数的语句”。
比较两种方式,第一种直接耦合了两个组件;而第二种只是在父组件中定义了可能会放置子组件的空位,实际嵌入哪个子组件是在主要组件中调用父组件的时候才确定下来的。第二种方式更有利于复用和维护复杂关系的组件。
总结
- 对于简单的组件通信场景,使用除了
$ref
之外的任意方法都可以。 - 方法3和4尚未充分使用,无法准确评估两者。其中,方法3对于嵌套层级较多的组件系统可能存在隐患,因为
$attr
可能会在向下传递的过程中被过滤,导致最终获取到的属性不完整甚至不存在。方法4 依赖注入相当于将提供provide
数据的组件作为状态管理者,负责组件间的通信。 - 综合使用
prop
和插槽可以一定程度上提高复用性和可维护性,适合动态结构的嵌套组件。目前还在尝试中,如果可行,后面会专门写一篇来分析这种方式。 - 对于需要高复用和高可维护的大型组件系统,建议使用代理接口乃至专门的状态管理库。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构