前端|Vue组件六种通信方式的优缺点及应用场景

前言 在做vue项目中,组件之间的通信是必不可少的,但是vue有多少种方式、用法、优缺点及适用场景,本篇文章大概总结一下。
本篇摘抄Vue组件通信方式及其应用场景总结并根据自己的理解做了修改)
一、props 1.基本用法 props通信方式是大家常见的通信类型,也是父子组件通讯的常用方式,用法是直接在子组件标签中绑定属性和方法。用props拿到声明的属性。对于父组件的方法,可以通过this.$emit触发。
示例
父组件:

> import son from './son' export default { name:'father', components:{ son /* 子组件 */ }, data(){ return { mes:'', sendSonMes:'',/* 发送给子组件的信息*/ sonMes:''/* 来自子组件的信息 */ } }, methods:{ /* 传递给子组件 */ send(){ this.sendSonMes = this.mes }, /* 接受子组件信息 */ sonSay(value){ this.sonMes = value }, }, }

前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

我们在父组件中只需要吧要传给子组件的数据fatherMes和提供子组件的方法sonSay通过标签方式传递给子组件
子组件:
> export default { name:'son', props:{ fatherMes:{ type:String, default:'' } }, data(){ return { mes:'' } }, methods:{ send(){ this.$emit('sonSay',this.mes) } }, } >

前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

对此我们还可以对props数据进行数据格式化,用computed接收props并格式化想要的数据类型。
例如在子组件进行处理
> export default { name:'son', props:{ fatherMes:{ type:String, default:'' } }, data(){ return { mes:'' } }, methods:{ send(){ this.$emit('sonSay',this.mes) } }, computed:{ computedFatherMes(){ return this.fatherMes + '' // 这里对父组件传来的数据做进一步处理 } }, } >

2.优点 props传递数据的优点显而易见、灵活简单,可以对props数据进行数据计算、数据监听等处理,十分灵活方便,但这里单单只是父子一层。
3.缺点 a.props篡改
我们子组件中使用父组件props的时候,如果涉及到一些变量赋值,修改等操作,props被莫名其妙的修改了,连同父组件的数据也被篡改了,这让我们很疑惑,父组件的props不是不能修改吗?这里怎么变了,
其实在vue中的props能不能改变,这个得分情况。
props如果是基础数据类型,当改变时,就会抛出错误:
前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

当props是引用类型的话,我们修改这个数据的某一个属性的实话,就可以。
由此我们可以得出结论:子组件虽然不能直接对父组件prop进行重新赋值,但父组件是引用类型的时候,子组件可以修改父组件的props下面的属性。
这就很尴尬了。如果我们设计的初衷就是父组件数据也能同时被修改,这个结果可以接受,如果不希望父组件的数据有变化是,这就是一个严重的逻辑bug。这就是props通讯的风险之一。
b.跨层级通讯,兄弟组件通信困难
对于父组件–子组件–子组件–子组件这种跨层级的通信,显然需要我们一层一层的props绑定属性和方法,如果遇到更复杂的情况,实现起来比较困难。如下图所示:
前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

对于兄弟组件之间的通讯,props需要通过父组件作为桥梁,实现子组件->父组件->子组件的通讯模式,如果想要通过父组件做媒介,那么必定会造成父组件重新渲染,为了实现兄弟组件通信付出的代价也比较大。如下图所示:
前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

4.应用场景 props的应用场景很简单,就是嵌套不深的父子组件通信,和关系不是很复杂的兄弟组件的通信。
二、this.$xxxx 此方法是通过this下面的数据直接获取vue实例。这种方法比较暴力,因为我们所谓的组件,最终都会是一个对象,存放组件的各种信息,组件和组件通过this.$children和this.$parent指针关联起来。因为在项目中只有一个root根组件,理论上,我们可以通过this.$children、this.$parent来访问页面上的任何一个组件,但实际上如果精确匹配到目标组件,确实一个无比棘手的问题。
1.基本用法 父组件:
> import son from './son' import son2 from './son2' export default { name:'father', components:{ son ,/* 子组件 */ son2 }, data(){ return { mes:'', sendSonMes:'', /* 来自子组件的信息 */ sonMes:''/* 发送给子组件的信息*/ } }, methods:{ /* 传递给子组件 */ send(){ /* 因为son组件是第一个有效组件所以直接去下标为0的组件 */ const currentChildren = this.$children[0] // 父组件直接调用this.$children获取到的值是一个由组件组成的数组 currentChildren.accept(this.mes) }, /* 接收子组件的信息 */ accept(value){ this.sonMes = value } }, }

子组件:
> export default { name:'son', data(){ return { mes:'', fatherMes:'' } }, methods:{ /* 接受父组件内容 */ accept(value){ this.fatherMes = value }, /* 向父组件发送信息 */ send(){ this.$parent.accept(this.mes) //子组件一般只有一个父组件,所以这里可以用this.$parent获取到父组件对象 }, }, }

我们可以看到,与props通信相比,这种方式显得更简洁,无需给子组件绑定事件和属性,只需要在父子组件上声明发送和接受数据的方法,就可以实现组件间的通信,看起来很是便捷,但是实际操作中会有很大的弊端,而却vue本身也不提倡这种通信方式。
2.优点 简单、方便 this.$children、this.$parent 还有this.$refs这种方式,更加简单直接获取vue实例,对vue实例下的数据和方法直接获取或者引用。
3.缺点 a. this.$children不可控性大,有一定风险
如上面的例子,如果我们在父组件稍微改变一点就会直接报错
之前的
v-if="false" />

改为
v-if="true" />

就会报错:
前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

这个样的错误,原因很简单,我们用 c h i l d r e n 的 下 标 获 取 , 但 是 兄 弟 组 件 s o n 2 v ? i f = t r u e 之 后 , 我 们 通 过 t h i s . children的下标获取,但是兄弟组件son2 v-if=true之后,我们通过this. children的下标获取,但是兄弟组件son2v?if=true之后,我们通过this.children[0]获取的是son2组件,该组件没有绑定方法,所以得出结论,对于v-if动态控制组件显示隐藏的不建议用this.$children用法取而代之的我们可以用ref获取对应子组件的实例
上面的可以改成
ref="son" />

然后获取:
const currentChildren = this.$refs.son

就根本解决了问题
b. 不利于组件化
直接获取组件实例这种方式,在一定程度上妨碍了组件化开发,组件化开发过程中,哪些方法提供给外部、哪些方法是内部使用。在没有提前商量的情况下,父子组件状态不透明的情况下,一切都是未知的。所以不同开发人员在获取组件下的方法时候,存在风险,提供的组件方法和属性是否有一些内部耦合。组件开发思路的初衷,是由组件内部的改变,来通知外部绑定的方法事件,而不是由外部来对内部做出一定的改变。反过来,如果子组件内部,主动向父组件传递一些信息,也不能确定父组件是否存在。
c. 兄弟组件深层次嵌套组件通讯困难
和props方式一样,如果是兄弟直接组件的通信,需要通过父组件作为中间通讯的桥梁,而深层次的组件通讯,虽然不需要像props通讯那样逐层绑定,但是有一点,需要逐渐向上层或者下层获取目标实例,如何精准获取这是一个非常头疼的问题,而且每当深入一层,风险性和不确定性会逐级扩大。
4.应用场景 【前端|Vue组件六种通信方式的优缺点及应用场景】该方式适合已知的、固定的页面结构,要求父子组件高度透明,知己知彼,很明确父子组件有哪些方法属性,有什么作用。所以说这种方式更适合页面组件,而不适合一些第三方组件库,或者是公共组件。
三、provide inject 此方法是在父组件上通过provide 将方法,属性,或者是自身实例暴露出去,子孙组件、插槽组件,甚至是子孙组件的插槽组件,通过inject把父辈provide引进来。提供给自己使用,很经典的应用的案例就是element-ui中el-form和el-form-item
比如我们在后台经常用的场景:

我们可以看到el-form和el-form-item不需要简历任何通信操作,那么el-form和el-form-item是如果联系起来,并且共享状态的呢?
1.基本用法 普通方式
我们用父组件->子组件->孙组件的案例
父组件
> import son from './son' export default { name:'father', components:{ son /* 子组件 */ }, provide(){ return { /* 将自己暴露给子孙组件 ,这里声明的名称要于子组件引进的名称保持一致 */ father:this } }, data(){ return { grandSonMes:'', /* 来自子组件的信息 */ sonMes:''/* 发送给子组件的信息*/ } }, methods:{ /* 接受孙组件信息 */ grandSonSay(value){ this.grandSonMes = value }, /* 接受子组件信息 */ sonSay(value){ this.sonMes = value }, }, }

这里我们通过provide把本身暴露出去。这里声明的名称要与子组件引进的名称保持一致。
子组件
> importgrandSon from './grandSon' export default { /* 子组件 */ name:'son', components:{ grandSon /* 孙组件 */ }, data(){ return { mes:'' } }, /* 引入父组件 */ inject:['father'], methods:{ send(){ this.father.sonSay(this.mes) } }, }

子组件通过inject把父组件实例引进来,然后可以直接通过this.father可以直接获取父组件,并调用下面的sonSay方法。
孙组件
> export default { /* 孙组件 */ name:'grandSon', /* 引入爷爷组件 */ inject:['father'], data(){ return { mes:'' } }, methods:{ send(){ this.father.grandSonSay( this.mes ) } } }

孙组件引入的方法和子组件一致。
插槽方式
provide、inject同样可以应用在插槽上,我们可以稍微改变一下,看示例:
父组件
> import son from './slotSon'import grandSon from './grandSon' export default { name:'father', components:{ son, /* 子组件 */ grandSon /* 孙组件 */ }, provide(){ return { /* 将自己暴露给子孙组件 */ father:this } }, data(){ return { grandSonMes:'', /* 来自子组件的信息 */ sonMes:''/* 发送给子组件的信息*/ } }, methods:{ /* 接受孙组件信息 */ grandSonSay(value){ this.grandSonMes = value }, /* 接受子组件信息 */ sonSay(value){ this.sonMes = value }, }, }

子组件

修改后达到了同样的效果,实际这种插槽模式,都是在父组件注册的组件,最后孙组件也会绑定到子组件的children下面。和上面基本用法的情况差不多。
provide其他用法
provide不仅能把整个父组件实例全部暴露出去,也可以根据需要只暴露一部分(就是该组件的一些属性或者方法)。例如,在上面的例子中,后代组件只用到了父组件的方法,所以我们可以只提供两个通信方法。但是需要注意的是,如果我们向外提供了方法,如果方法中有操作this行为。示例
父组件:
provide(){ return { /* 将通信方法暴露给子孙组件(注意绑定this) */ grandSonSay:this.grandSonSay.bind(this), sonSay:this.sonSay.bind(this) } }, methods:{ /* 接受孙组件信息 */ grandSonSay(value){ this.grandSonMes = value }, /* 接受子组件信息 */ sonSay(value){ this.sonMes = value }, }

子组件
/* 引入父组件方法 */ inject:['sonSay'], methods:{ send(){ this.sonSay(this.mes) } },

2.优点 组件传值示意图
前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

a. 组件通信不受到子组件层级的影响
无论组件嵌套多少层级,都可以不受到层级的影响
b. 适用于插槽和嵌套插槽
provide-inject 让插槽嵌套的父子组件通信变得简单,这就是刚开始我们说的,为什么el-form和el-form-item能够协调管理表单的状态一样。在elemnt源码中,el-form就是将this本身provide出去的
缺点 a. 不适合兄弟通讯
provide-inject协调作用就是获取父级组件们提供的状态、方法和属性等,流向一直都是由父到子,provide提供内容不可能被兄弟组件获取到的,所以兄弟组件的通信不可能靠这种方式来完成。
b. 父级组件无法主动通信
provide-inject更像父亲挣钱给儿子话一样,儿子可以从父亲这里拿到提供的条件,但是父亲却无法向儿子索取任何东西。正如这个比方,父组件对子组件的状态一无所知。也不能主动发起通信。
4.应用场景 provide-inject这种通信方式,更适合深层次复杂的父子代通信,子孙组件可以共享父组件的状态,还有一点就是适合el-form、el-form-item这种插槽类型的情景。
四、Vuex vuex算是vue中处理复杂组件通信的最佳方案,毕竟vue和vuex一个娘胎里出来的。而且vuex底层也是用vue实现的。
示意图:
前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

基本用法 vuex文件:
import Vuex from 'vuex' import Vue from 'vue'Vue.use(Vuex)export default new Vuex.Store({ state:{ fatherMes:'', sonMes:'', fatherMesAsync:'' }, mutations:{ sayFaher(state,value){ state.fatherMes = value }, saySon(state,value){ state.sonMes = value }, sayAsyncFather(state,value){ state.fatherMesAsync = value } }, actions:{ asyncSayFather({ commit },payload){ return new Promise((resolve)=>{ setTimeout(()=>{ resolve(payload) },2000) }).then(res=>{ commit('sayAsyncFather',res) }) } } })

前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

在store文件中,我们声明三个mutations分别是向父组件通信saySon、父组件向子组件通信的同步方法sayFather和异步方法sayAsyncFather,actions中模拟了一个三秒后执行的异步任务asyncSayFather。
main.js注入store
import store from './components/vuex/store'new Vue({ render: h => h(App), store }).$mount('#app')

父组件
> import son from './son' export default { /* 父组件 */ name:'father', components:{ son ,/* 子组件 */ }, data(){ return { mes:'', asyncMes:'' } }, computed:{ sonMes(){ return this.$store.state.sonMes // 读取vuex中的子组件状态数据 } }, mounted(){ console.log(this.$store) }, methods:{ /* 触发mutations,传递数据给子组件 */ send(){ this.$store.commit('sayFaher',this.mes) //这里触发同步函数去改变对应的值 }, /* 触发actions,传递数据给子组件 */ asyncSend(){ this.$store.dispatch('asyncSayFather',this.asyncMes)// 这里的异步触发异步函数来改变对应的值 } }, }

父组件分别触发同步异步方法,把信息发送给子组件。用computed来接收vuex中的state
子组件
> export default { name:'son', data(){ return { mes:'', } }, computed:{ /* 接受父组件同步消息 */ fatherMes(){ return this.$store.state.fatherMes }, /* 接受父组件异步消息 */ fatherMesAsync(){ return this.$store.state.fatherMesAsync } }, methods:{ /* 向父组件发送信息 */ send(){ this.$store.commit('saySon',this.mes) }, }, }

子组件的方法和父组件保持一致
2.优点 (1)根本解决复杂组件的通信问题
Vuex在一定程度上根本解决了vue复杂的组件通信问题,我们可以不再关心两个毫无干系的两个组件的通信问题了。
(2)支持异步组件通信
Vuex中actions允许我们做一些异步操作,然后通过commit可以把数据传入对应的mutation。
3.缺点 流程相比较稍微复杂。
Vuex通信方式相比其他方式,比较复杂,而且如果不同的模块,需要建立独立的modules
4.开发场景 实际开发场景中,不会存在demo项目这样简单的通信,vuex的出现,就是解决这些比较复杂的组件通信场景。对于中大型项目,vuex是很不错的状态管理和数据通信方案。
五、事件总线 ---- EventBus EventBus事件总线,EventBus所有事件统一调度,有一个统一管理事件中心,一个组件绑定事件,另一个组件触发事件,所有的组件不再受到父子组件的限制,那个页面需要数据,就绑定事件,然后由数据提供者触发对应的事件了提供数据。
EventBus核心思想是事件的绑定和触发,这一点和vue中this. e m i t 和 t h i s . emit和this. emit和this.on一样,这个页是整个EventBus核心思想。下面我们举个例子
1.基本用法 EventBus.js
export default class EventBus { es = {} /* 绑定事件 */ on(eventName, cb) { if (!this.es[eventName]) { this.es[eventName] = [] } this.es[eventName].push({ cb }) } /* 触发事件 */ emit(eventName, ...params) { const listeners = this.es[eventName] || [] let l = listeners.lengthfor (let i = 0; i < l; i++) { const { cb } = listeners[i] cb.apply(this, params) } } }export default new EventBus()

这就是一个简单的事件总线,有on,emit两个方法。
父组件
> import son from './son' import brotherSon from './brother' import EventBus from './eventBus' export default { name:'father', components:{ son ,/* 子组件 */ brotherSon, /* 子组件 */ }, data(){ return { mes:'', sonMes:''/* 发送给子组件的信息*/ } }, mounted(){ /* 绑定事件 */ EventBus.on('sonSay',this.sonSay) }, methods:{ /* 传递给子组件 */ send(){ EventBus.emit('fatherSay',this.mes)// 重点,一定以绑定该组件需要监听的事件参数,第一个事件名,第二个是需要执行的函数 }, /* 接受子组件信息 */ sonSay(value){ this.sonMes = value }, }, }

我们在初始化的时候通过EventBus的on方法绑定sonSay方法供给给子组件使用。向子组件传递信息 的时候,通过emit触发子组件的绑定方法,实现了父子通信。
子组件
> import EventBus from './eventBus' export default { name:'son', data(){ return { mes:'', brotherMes:'', fatherMes:'' } }, mounted(){ /* 绑定事件 */ EventBus.on('fatherSay',this.fatherSay) }, methods:{ /* 向父组件传递信息 */ send(){ EventBus.emit('sonSay',this.mes) }, /* 向兄弟组件传递信息 */ sendBrother(){ EventBus.emit('brotherSay',this.brotherMes) }, /* 父组件对我说 */ fatherSay(value){ this.fatherMes = value } } }

和父组件的逻辑差不多,把需要接受数据的方法,通过EventBus绑定,通过触发eventBus方法,来向外部传递信息。我们还模拟了兄弟之间通信的场景。
兄弟组件
>import EventBus from './eventBus' export default { /* */ name:'brother', data(){ return { brotherMes:'' } }, mounted(){ /* 绑定事件给兄弟组件 */ EventBus.on('brotherSay',this.brotherSay) }, methods:{ brotherSay(value){ this.brotherMes = value } } }

我们看到,兄弟组件处理逻辑和父子之间没什么区别。
2.优点 (1)简单灵活,父子兄弟组件通信不受到限制
EventBus的通信方式,相比之前的几种比较简单,而且不受到组件层级的影响,可以实现任意两个组件的通信。需要数据就通过on绑定,传递数据就emit触发。
(2)通信方式不受框架影响
EventBus的通信方式,不只是vue可以用,react、小程序都可以用这种通信方式,而且笔者感觉这种通信方式更适合小程序通信,后面会讲到为什么
(3)不利于组件化开发
EventBus通信方式是无法进行有效的组件化开发的,假设一个场景,一个页面上有多个公共组件,我们只要向其中的一个传递数据,但是每个公共组件都绑定了数据接收的方法。我们怎么样做到吧数据传递给需要的组件呢?
4.应用场景 前端|Vue组件六种通信方式的优缺点及应用场景
文章图片

实现总线这种方式更适合微信小程序和基于vue构建的小程序,至于为什么。因为我们都知道小程序采用双线程模型(渲染层+逻辑层)如上图所示,渲染层作用就是小程序wxml渲染到我们的视线中,而逻辑层就是我们写的代码逻辑,在性能上,我们要知道在渲染层浪费的性能要远大于逻辑层的代码执行性能开销,如果我们在小程序里采用通过props等传递方式,属性是绑定在小程序标签里面的,所以势必要重新渲染视图层。如果页面结构复杂,可能会造成卡顿等情况,所以我们通过eventBus可以绕过渲染层,直接由逻辑层将数据进行推送,节约了性能的开销。
六、事件总线 ---- new Vue new Vue 这种通信方式和eventBus大致差不多,有一点不同的是,以vue实例作为eventBus中心,除了我们可以用$on,$emit之外,我们还可以用vue下的data,watch等方法,而且我们可以建立多个vue作为不同模块的数据通信桥梁,相比上边eventBus方法,new Vue这种方法更高效,更适合vue项目场景。
1.基本使用 VueBus
import Vue from 'vue'export default new Vue()

父组件
> import son from './son' import VueBus from './vueBus' export default { /* 父组件 */ name:'father', components:{ son ,/* 子组件 */ }, data(){ return { mes:'', sonMes:'' /* 发送给子组件的信息*/ } }, created(){ /* 绑定属性 */ VueBus._data.mes = 'hello,world' }, mounted(){ /* 绑定事件 */ VueBus.$on('sonSay',this.sonSay) }, methods:{ /* 传递给子组件 */ send(){ VueBus.$emit('fatherSay',this.mes) }, /* 接受子组件信息 */ sonSay(value){ this.sonMes = value }, }, }

我们通过$on绑定了接受数据的方法,初始化的时候向vue._data下面绑定了数据。
子组件
> import VueBus from './vueBus' export default { name:'son', data(){ return { mes:'', brotherMes:'', fatherMes:'' } }, mounted(){ /* 绑定事件 */ VueBus.$on('fatherSay',this.fatherSay) }, methods:{ /* 向父组件传递信息 */ send(){ VueBus.$emit('sonSay',this.mes) }, /* 父组件对我说 */ fatherSay(value){ this.fatherMes = value }, /* 获取父组件存入vue中的数据 */ getFatherMes(){ console.log( VueBus._data.mes ) } }, }

和eventBus时间总线一样,我们可以直接通过_data数据直接获取到父组件传递的内容。
2.优点 (1)简单灵活,任意组件可以通信
和上边eventBus通信方式一样,这种通信方式很灵活,可以轻松在任意组件间实现通信
(2)除了通信还可以使用watch,computed等方法
如果我们通过vue作为通信媒介,那么只用其中的 e m i t 和 emit和 emit和on真的是有点大材小用了,既然实例了一个vue,我们可以轻松的使用vue的$watch computed等功能。
3.缺点 基本上EventBus的缺点,都在vue这种通信方式中都有存在
4.应用场景 在项目中不考虑用vuex的中小项目中,可以考虑采用vue事件总线这种通信方式,在使用这种方式的时候,我们一定要注意命名空间,不要重复绑定事件名称。分清楚业务模块,避免后续维护困难。
最后,我们在项目中,具体要用什么通信方式,还要看具体的业务场景,项目大小等因素综合评估。文章中介绍了vue通信方式的优缺点,可以给大家实际工作中提供一个参考。

    推荐阅读