前言
相信实际项目中用过vue的同学,一定对vue中父子组件之间的通信并不陌生,vue中采用良好的数据通讯方式,避免组件通信带来的困扰。今天笔者和大家一起分享vue父子组件之间的通信方式,优缺点,及其实际工作中的应用场景
首先我们带着这些问题去思考
1 vue中到底有多少种父子组件通信方式?
2 vue中那种父子组件最佳通信方式是什么?
3 vue中每个通信方式应用场景是什么?
一 prop
1 基本用法
prop
通信方式大家最常见的,也是最常用的父子组件通信类型,我们可以直接在标签里面给子组件绑定属性和方法,对于属性我们可以直接通过子组件声明的prop
拿到,对于父元素的方法,我们可以通过 this.$emit触发
。
我们先简单写一个props
父子组件通信的场景
父组件
<template>
<div class="father" >
<input v-model="mes" /> <button @click="send" >对子组件说</button>
<div>子组件对我说:{
{ sonMes }}</div>
<son :fatherMes="sendSonMes" @sonSay="sonSay" />
</div>
</template>
<script>
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
},
},
}
</script>
我们这里只需要将给子组件的数据fatherMes
和提供给子组件的方法 sonSay
通过标签方式传递给子组件。
子组件
<template>
<div class="son" >
<div> 父组件对我说:{
{ fatherMes }} </div>
<input v-model="mes" /> <button @click="send" >对父组件说</button>
</div>
</template>
<script>
export default {
name:'son',
props:{
fatherMes:{
type:String,
default:''
}
},
data(){
return {
mes:''
}
},
methods:{
send(){
this.$emit('sonSay',this.mes)
}
},
}
</script>>
子组件通过props
来定义接受父组件传递过来的信息,我们就可以直接通过this
获取到,对于父组件传递的事件,我们可以通过this.$emit
来触发事件。
区别react的props
这里和react
的props
有点区别,react
组件更新来源于props
更新和自身state
改变,当props
改变,会默认更新组件。而在Vue
中,如果我们对父组件传过来的新的props
没有做依赖收集(template
模版收集,computed
计算属性收集),组件是不会触动更新的。
效果
数据格式化
如果我们想对props
数据进行数据格式化,那么用computed
接受props
并格式化想要的数据类型。
<div class="son" >
<div> 父组件对我说:{
{ computedFatherMes }} </div>
<input v-model="mes" /> <button @click="send" >对父组件说</button>
</div>
export default {
name:'son',
props:{
fatherMes:{
type:String,
default:''
}
},
computed:{
computedFatherMes(){
return this.fatherMes + '😄😄'
}
},
}
数据监听
当我们根据props
改变不想更新视图,或者不想立即更新视图,那么可以用watch
来监听来自父组件的props
的变化。
watch:{
fatherMes(newVaule){
console.log( newVaule)
}
},
2优点
props
传递数据优点是显而易见的,灵活简单,可以对props
数据进行计算属性,数据监听等处理。父子组件通信灵活方便。这里可能单单仅限父子一层。
3缺点
1 props篡改
我们在子组件中使用父组件props
的时候,如果涉及一些变量赋值,修改等操作,props
被莫名其妙的修改了,连同父组件的数据也被篡改了,有的同学可能会很疑惑,父元素的props
不是不能修改么,这里怎么改变了呢,vue
中props
到底能不能改变?,至于这个问题,不能直接给出答案,如果props
是基础数据类型,当我们改变的时候,就会曝出错误。我们一起看一个例子。
父组件
<template>
<div>
<div>父组件</div>
<son :fData="sonProps" />
</div>
</template>
<script>
import son from './sTampering'
export default {
name:'father',
components:{
son
},
data(){
return {
sonProps:''
}
},
}
</script>
子组件
<template>
<button >改变父组件props</button>
</template>
<script>
export default {
name:'son',
props:{
fData:{
required:true
}
},
methods:{
changFatherProps(){
this.fData = 'hello ,father'
}
}
}
</script>
当我们直接点击button 会抛出以下警。
但是当我们传过来的是一个引用数据类型,并且修改数据下某一个属性的时候。
父组件
data(){
return {
sonProps:{
a:1,
b:2
}
}
},
子组件
changFatherProps(){
this.fData.a = 'hello,world'
}
当点击按钮发现并没有报错。
于是我们打印父组件的sonprops
数据:
我们发现父组件的data
数据已经被子组件篡改。于是我们总结出一个结论,子组件虽然不能直接对父组件prop
进行重新赋值,但是当父组件是引用类型的时候,子组件可以修改父组件的props
下面的属性。这就是一个很尴尬的事情,如果我们设计的初衷就是父组件数据也同时被修改,这个结果是可以接受的,但是当我们不希望父组件那份数据源有任何变化的时候,这就是一个严重的逻辑bug
。
所以这就是props
通讯的风险项之一。
2 跨层级通信,兄弟组件通讯困难
对于父组件-子组件-子组件的子组件这种跨层级的通信,显然需要我们一层一层的prop
绑定属性和方法,如果遇到更复杂的情况,实现起来比较困难。
对于兄弟组件之间的通讯,props
需要通过父组件作为桥梁,实现子组件-> 父组件 -> 子组件通信模式,如果想要通过父组件做媒介,那么必定会造成父组件重新渲染,为了实现兄弟组件通信付出的代价比较大。
4 应用场景
props
的应用场景很简单,就是正常不是嵌套很深的父子组件通信,和关系不是很复杂的兄弟组件组件通信。
二 this.$xxx
通过this下面的数据直接获取vue
实例这种方法比较暴力,因为我们所谓的组件,最终都会是一个对象,存放组件的各种信息,组件和组件通过this.$children`和`this.$parent
指针关联起来。因为在项目中只有一个root
根组件,理论上,我们可以找到通过this.$children` `this.$parent
来访问页面上的任何一个组件 ,但是实际上如何精确匹配到目标组件,确是一个无比棘手的问题。
1 基本用法
父组件
<template>
<div class="father" >
<input v-model="mes" /> <button @click="send" >对子组件说</button>
<son2 v-if="false" />
<div>子组件对我说:{
{ sonMes }}</div>
<son />
</div>
</template>
<script>
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]
currentChildren.accept(this.mes)
},
/* 接收子组件的信息 */
accept(value){
this.sonMes = value
}
},
}
</script>
子组件
<template>
<div class="son" >
<div> 父组件对我说:{
{ fatherMes }} </div>
<input v-model="mes" /> <button @click="send" >对父组件说</button>
</div>
</template>
<script>
export default {
name:'son',
data(){
return {
mes:'',
fatherMes:''
}
},
methods:{
/* 接受父组件内容 */
accept(value){
this.fatherMes = value
},
/* 向父组件发送信息 */
send(){
this.$parent.accept(this.mes)
},
},
}
</script>
效果
我们可以清楚看到,和props
通信相比,this.$parent`,`this.$children
显得更加简洁,无需再给子组件绑定事件和属性,只需要在父组件和子组件声明发送和接受数据的方法。就可以实现组件间的通信,看起来很是便捷,但是实际操作中会有很大的弊端,而且vue
本身也不提倡这种通信方式。而且这种通信方式也有很多风险性,我们稍后会给予解释。
2 优点
简单,方便this.$children`,`this.$parent
this.$refs
这种通信方式,更加的简单直接获取vue实例,对vue实例下的数据和方法直接获取或者引用。
3 缺点
1 $this.children不可控性大,有一定风险
如上的例子,我们稍微对父组件加以改动,就会直接报错。
之前的
<son2 v-if="false" />
改成
<son2 v-if="true" />
就会报如上错误,错误的原因很简单,我们用 $children` 的下标获取,但是兄弟组件`son2` `v-if = true` 之后,我们通过通过`this.$children[0]
获取的就是son2
组件,son2没有绑定方法,所以得出结论,对于v-if
动态控制组件显示隐藏的不建议用this.$children
用法,取而代之的我们可以用ref
获取对应子组件的实例。
如上改成
<son ref="son" />
然后获取:
const currentChildren = this.$refs.son
根本解决了问题。
2 不利于组件化
直接获取组件实例这种方式,在一定程度上妨碍了组件化开发,组件化开发的过程中,那些方法提供给外部,那些方法是内部使用,在没有提前商量的情况下,父子组件状态不透明的情况下,一切都是未知的,所以不同开发人员在获取组件下的方法时候,存在风险,提供的组件方法,属性是否有一些内部耦合。组件开发思路初衷,也不是由组件外部来对内部作出一定改变,而往往是内部的改变,通知外部绑定的方法事件。反过来如果是子组件内部,主动向父组件传递一些信息,也不能确定父组件是否存在。
3 兄弟组件深层次嵌套组件通讯困难
和props方式一下,如果是兄弟直接组件的通信,需要通过父组件作为中间通讯的桥梁,而深层次的组件通讯,虽然不需要像props通讯那样逐层绑定,但是有一点,需要逐渐向上层或下层获取目标实例,如何能精准获取这是一个非常头疼的问题,而且每当深入一层,风险性和不确定性会逐级扩大。
4 应用场景
直接通过实例获取的通信方式适合已知的,固定化的页面结构,这种通讯方式,要求父子组件高度透明化,知己知彼,很明确父子组件有那些方法属性,都是用来干什么。所以说这种方式更适合页面组件,而不适合一些第三方组件库,或者是公共组件。
三 provide inject
如果说vue
中 provide
和 inject
,我会首先联想到react
的context
上下文,两个作用在一定程度上可以说非常相似,在父组件上通过provide
将方法,属性,或者是自身实例暴露出去,子孙组件,插槽组件,甚至是子孙组件的插槽组件,通过inject
把父辈provide
引进来。提供给自己使用,很经典的应用 provide
和 inject
的案例就是 element-ui
中 el-form
和 el-form-item
我们先不妨带着问题想象一个场景
<el-form label-width="80px" :model="formData">
<el-form-item label="姓名">
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="年龄">
<el-input v-model="formData.age"></el-input>
</el-form-item>
</el-form>
我们可以看到 el-form
和 el-form-item
不需要建立任何通信操作,那么el-form
和el-form-item
是如何联系起来,并且共享状态的呢?我们带着疑问继续往下看。
1 基本用法
普通方式
我们用父组件 -> 子组件 -> 孙组件 的案例
父组件
<template>
<div class="father" >
<div>子组件对我说:{
{ sonMes }}</div>
<div>孙组件对我说:{
{ grandSonMes }}</div>
<son />
</div>
</template>
<script>
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
},
},
}
</script>
这里我们通过provide
把本身暴露出去。⚠️⚠️⚠️这里声明的名称要与子组件引进的名称保持一致
子组件
<template>
<div class="son" >
<input v-model="mes" /> <button @click="send" >对父组件说</button>
<grandSon />
</div>
</template>
<script>
import grandSon from './grandSon'
export default {
/* 子组件 */
name:'son',
components:{
grandSon /* 孙组件 */
},
data(){
return {
mes:''
}
},
/* 引入父组件 */
inject:['father'],
methods:{
send(){
this.father.sonSay(this.mes)
}
},
}
</script>
子组件通过inject
把父组件实例引进来,然后可以直接通过this.father
可以直接获取到父组件,并调用下面的sonSay
方法。
孙组件
<template>
<div class="grandSon" >
<input v-model="mes" /> <button @click="send" >对爷爷组件说</button>
</div>
</template>
<script>
export default {
/* 孙组件 */
name:'grandSon',
/* 引入爷爷组件 */
inject:['father'],
data(){
return {
mes:''
}
},
methods:{
send(){
this.father.grandSonSay( this.mes )
}
}
}
</script>
孙组件没有如何操作,引入的方法和子组件一致。
效果
2 插槽方式
provide , inject
同样可以应用在插槽上,我们给父子组件稍微变动一下。
父组件
<template>
<div class="father" >
<div>子组件对我说:{
{ sonMes }}</div>
<div>孙组件对我说:{
{ grandSonMes }}</div>
<son >
<grandSon/>
</son>
</div>
</template>
<script>
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
},
},
}
</script>
子组件
<template>
<div class="son" >
<input v-model="mes" /> <button @click="send" >对父组件说</button>
<slot />
</div>
</template>
达到了同样的通信效果。实际这种插槽模式,所在都在父组件注册的组件,最后孙组件也会绑定到子组件的children下面。和上述的情况差不多。
provied其他用法
provide
不仅能把整个父组件全部暴露出去,也能根据需要只暴露一部分(一些父组件的属性或者是父组件的方法),上述的例子中,在子孙组件中,只用到了父组件的方法,所以我们可以只提供两个通信方法。但是这里注意的是,如果我们向外提供了方法,如果方法里面有操作this
行为,需要绑定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 优点
1 组件通信不受到子组件层级的影响
provide inject
用法 和 react.context
非常相似, provide
相当于Context.Provider
,inject
相当于 Context.Consumer
,让父组件通信不受到组件深层次子孙组件的影响。
2 适用于插槽,嵌套插槽
provide inject
让插槽嵌套的父子组件通信变得简单,这就是刚开始我们说的,为什么 el-form
和 el-form-item
能够协调管理表单的状态一样。在element
源码中 el-form
就是将this
本身provide
出去的。
3 缺点
1 不适合兄弟通讯
provide-inject
协调作用就是获取父级组件们提供的状态,方法,属性等,流向一直都是由父到子,provide
提供内容不可能被兄弟组件获取到的,所以兄弟组件的通信不肯能靠这种方式来完成。
2 父级组件无法主动通信
provide-inject
更像父亲挣钱给儿子花一样,儿子可以从父亲这里拿到提供的条件,但是父亲却无法向儿子索取任何东西。正如这个比方,父组件对子组件的状态一无所知。也不能主动向子组件发起通信。
4 应用场景
provide-inject
这种通信方式,更适合深层次的复杂的父子代通信,子孙组件可以共享父组件的状态,还有一点就是适合el-form
el-form-item
这种插槽类型的情景。
总结
在下一节中,我们将继续介绍 vue 中的通信方式。