VUE组件间通信方式总结
一、面试题
不用多说,参加过前端面试的小伙伴应该都遇到过,很经典的面试题。下面这些组件间的通信方式不但要会代码实现,还要知道每种方式的优缺点以及适用场景,学会灵活运用。
听别人说过一句话,原文忘记了,只记得大概是说:架构师在技术选型时不是选择最好最前沿的技术,而是选择项目最适合的方案。可能vue
,react
很好,但是有些项目可能更适合jsp
,这是业务场景决定的。因此我们需要熟悉各个技术的特点,灵活的选用最合适的。
关于每一种通信方式的优缺点总结,是自己平时开发和工作中总结出来的,并没有参考其余博客,可能不是特别全面,大家做个参考,可以结合自己的经验进行思考,也欢迎大家提出自己的意见
二、$emit + props
最常用的通信方式,适用于直接父子间的通信。vue
是单向数据流,也就是数据只能从父组件流向子组件,这也就是为什么子组件直接修改props
会警告。
父组件通过props
将值传递到子组件,子组件通过$emit('event', payload)
将数据作为payload(负载)
传递回父组件
案例: 父组件
1 | <!-- 父组件 --> |
子组件
1 | <template> |
这样就完成了父子组件通过props
将name
传递到子组件,子组件通过$emit
方法,将payload
传递到父组件
如果是兄弟节点之间通信可以通过他们共同的父组件作为媒介,一个子组件将消息先发送到父组件,再由父组件将消息发送给另一个子组件,但是并不推荐这样做。
优点:通信方式简单,适用父子组件间直接通信。
缺点:不适合跨组件通信方式(需要父组件作为媒介,代价较高)
三、事件总线
事件总线利用的是一种设计模式发布订阅者模式
,实现思路就是通过一个vue
实例作为载体,这个实例接受各方通过$emit
发出的各种事件,在需要的地方引入这个实例并通过$on
监听对应的事件,如果不需要了,在通过$off
移除这个事件,释放资源
有两种方式
3.1 修改原型链
直接修改全局vue
实例的原型链,这样可以随处访问,简单快速。
只需要在main.js
中加入这一行代码
1 | Vue.prototype.$bus = new Vue(); |
案例:两个兄弟节点之间通信
父组件:
1 | <template> |
ChildA
组件,发布事件
1 | <template> |
ChildB
组件,监听事件并展示响应内容
1 | <template> |
3.2 按需引入
另一个种是定义一个单独的js文件,只在需要的地方引入并监听。
1 | import Vue from 'vue'; |
不需要在main.js
引入,只需要在需要的地方引入。父组件不需要修改,修改ChildA
和ChildB
组件代码,在script标签最顶端引入这个js文件
1 | import Bus from '@/config/bus.js'; |
同时将ChildA
和ChildB
组件中this.$bus
更换为Bus
即可
优点:相对于
props
和$emit
的方式,事件总线可以轻松跨组件通信,实现方式也比较简单缺点:需要手动移除监听事件(通过
$off()
),如果想单独移除某个事件的处理函数,不能使用匿名处理函数,必须指定函数名,具体见api,这里不再赘述
四、$attrs 和 $listeners
先说用法,$attrs
可以接收到props
没有接收到的参数,$listeners
可以接收到除了.native
修饰的所有绑定的事件
这种方式平时业务开发应用场景比较少,目前个人接触更多的是二次封装组件时,后面会举例说明。
思想有点像继承,通过$attrs
来继承父组件提供的属性,通过$listeners
来继承父组件的方法(事件)。
举例,二次封装Element-UI
的数据表格el-table
,这里只是举例介绍用法,不会详细介绍如何具体封装
定义一个组件RamblerTable
,代码如下
1 | <template> |
父组件(调用)
1 | <template> |
可以看到表格已经有了表格和斑马纹,在使用时只需要将el-table
提供的属性(stripe, border等)定义rambler-table
身上就可以通过$attrs
穿透到子组件上
$listener
也是同理,只需要稍稍修改RamblerTable
1 | <el-table :data="list" v-bind="$attrs" v-on='$listeners'> |
通过v-on
指令将外层注册的事件穿透到el-table
身上,这样封装组件可以少些很多重复代码,而且可以让使用者直接阅读element-ui
的文档就知道可以使用什么属性和事件
优点:属性穿透,可以利用这个特性二次封装组件
缺点:只适用于父子组件,如果想要穿透多层子组件需要在每一层都通过
v-bind
和v-on
进行注册
五、Vuex
通过vuex可以做到和事件总线一样轻松跨组件通信,同时还支持响应式,使用也比较简单,除了特别小的项目,基本是vue
项目的标配
使用方法:通过分发(dispatch)action
,在action
中提交(commit)mutations
,在mutation
中修改state
的值,根据业务需要可能还需要对数据进行持久化(如sessionStorage
或者localStorage
)
如登录流程中,登录成功后,分发action
1 | this.$store.dispatch("user/login", response.data); |
在action
中提交mutation
1 | login({ commit}, data) { |
mutation
中修改state
并对数据进行持久化(登录可以考虑用sessionStorage
保存用户信息)
1 | USER_LOGIN(state, value) { |
关于vuex还有一个常考的问题:
action
和mutation
的区别是什么?答案是关于同步和异步问题,不知道的小伙伴可以查阅相关资料
优点:1. 响应式; 2. 跨组件通信方便; 3. 规范,易于统一管理
缺点:增加开发成本,需要定义
action
,mutation
等,还需要对数据进行持久化
六、provide 和 inject
父组件,通过provide选项提供一个数据,provide可以是一个对象也可以是返回对象的函数(类似data);子组件,通过inject选项注入到当前页面中,inject可以是一个字符串数组,也可以是一个对象(对象有多种形式)。
举个例子:
父组件
1 | <template> |
ChildA
直接通过对象数组接受
1 | <template> |
ChildB
通过对象接收,和props一样,可以给一个默认值
1 | <template> |
昨天和别人聊起这个provide和inject,因为他不是响应式,开发中可能用的不是这么多,特意去网上看了下如何将数据变成响应式。办法一般是两种,一个是将this传递过去,因为当前实例是响应式的,第二种是利用Vue提供的一个方法Vue.observable( object )。
今天又考虑这个问题,为什么一定要做到响应式呢,这个设计出来可能就不是为了这个目的,如果是为了响应式还要跨组件通信,那为什么不用vuex呢。我们需要做的就是结合业务考虑更适合的方式,毕竟通信方式这么多。
优点:无论层级多深,只要是子组件,都可以注入这个provide提供的数据,可以利用这一特性在App.vue中提供数据,也可以做到全局注入。传值方式比较形象
缺点:非响应式,只能向下层提供数据,限制了应用场景
七、$parent、$children以及ref
三者用法类似,都是通过获取组件实例来访问属性和方法进而实现通信。
$parent
获取当前组件的直接父组件,$children
获取当前组件的子组件(返回一个数组,通过下标访问),$refs
中保存着所有通过ref
注册的组件,因此可以直接通过$refs
获取到组件实例,获取到组件实例就可以访问实例的属性和方法。
这个比较简单,举个简单例子
父节点:
1 | <template> |
首先是父节点访问子节点
1 | // 访问child-b节点的name属性 |
子节点访问父节点
1 | // 访问父节点 |
子节点访问兄弟节点
1 | // 访问兄弟节点的name属性 |
这里和dom
操作类似。
优点:通过
$parent
和$children
基本可以访问到所有的节点,也可以访问属性以及调用方法。缺点:跨越层级较大时不方便;代码可读性较差(跨越层级较多时)
八、slot分发,具名插槽
父传子用slot
插槽,子传父用具名插槽,也勉强算是一种通信,毕竟父子组件确实访问了彼此的属性
在这之前贴一句官方文档的一句话,防止有些同学看的迷糊
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
8.1 父传子的例子
子组件只提供一个插槽
1 | <template> |
父组件
1 | <template> |
虽然h1
是在父组件作用域中编译的,但是父组件还是将h1
标签分发到子组件中,也算是父组件将数据传到子组件把(是在牵强)
8.2 子传父例子
比如我定义了一个页面布局,Layout
布局,预留出了Header
,Footer
的插槽
Layout.vue
定义了两个具名插槽,同时绑定了两个参数header
和footer-title
1 | <template> |
父组件调用:
Parent.vue
利用子组件的参数渲染页面,子组件通过一个函数将所有绑定的值通过一个对象返回,因此可以利用ES6
的解构语法,以及默认值的语法,见下面例子,由于子组件没有绑定noParam
这个参数,因此渲染出了默认值
1 | <template> |
这样定义布局也有个好处,可以将Header
和Footer
封装成组件,实现了软件的高可插拔性
,在需要时只需要替换对应的组件即可
优点:具名插槽可以让父组件访问子组件的属性,让封装组件更加灵活
缺点:通信方式比较简单,只能访问子组件绑定的属性
九、总结
大概就是这七种常用的,没有涵盖vue1.x中$dispatch
和 $broadcast
(在vue2.0已被移除)
也有人把v-model
作为一种通信方式,其实v-model
只是一种语法糖,本质上也是$emit
和props
这种方式
每种方式都有各自的特定,没有最好这一种说法,正如开头说的,无论是选择哪种方式,都要根据实际业务特点。脱离了业务谈技术选型没有任何意义。