Vue组件通信方式及其应用场景总结(上)

简介: Vue 中通信方式总结

前言

相信实际项目中用过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

这里和reactprops有点区别,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不是不能修改么,这里怎么改变了呢,vueprops到底能不能改变?,至于这个问题,不能直接给出答案,如果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

如果说vueprovideinject,我会首先联想到reactcontext上下文,两个作用在一定程度上可以说非常相似,在父组件上通过provide将方法,属性,或者是自身实例暴露出去,子孙组件,插槽组件,甚至是子孙组件的插槽组件,通过inject把父辈provide引进来。提供给自己使用,很经典的应用 provideinject的案例就是 element-uiel-formel-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-formel-form-item不需要建立任何通信操作,那么el-formel-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-formel-form-item能够协调管理表单的状态一样。在element源码中 el-form 就是将this本身provide出去的。

3 缺点

1 不适合兄弟通讯

provide-inject 协调作用就是获取父级组件们提供的状态,方法,属性等,流向一直都是由父到子,provide提供内容不可能被兄弟组件获取到的,所以兄弟组件的通信不肯能靠这种方式来完成。

2 父级组件无法主动通信

provide-inject更像父亲挣钱给儿子花一样,儿子可以从父亲这里拿到提供的条件,但是父亲却无法向儿子索取任何东西。正如这个比方,父组件对子组件的状态一无所知。也不能主动向子组件发起通信。

4 应用场景

provide-inject这种通信方式,更适合深层次的复杂的父子代通信,子孙组件可以共享父组件的状态,还有一点就是适合el-form el-form-item这种插槽类型的情景。

总结

在下一节中,我们将继续介绍 vue 中的通信方式。

相关文章
|
2天前
|
JavaScript 前端开发
Vue组件生命周期深度剖析:从创建到销毁的八大钩子实战指南
Vue组件生命周期深度剖析:从创建到销毁的八大钩子实战指南
|
1天前
|
JavaScript 前端开发 API
技术好文:vue混入(mixin)的使用
技术好文:vue混入(mixin)的使用
|
1天前
|
移动开发 JavaScript 程序员
程序员必知:vue播放video插件vue
程序员必知:vue播放video插件vue
|
1天前
|
JavaScript 程序员
程序员必知:vue中按钮使用v
程序员必知:vue中按钮使用v
|
1天前
|
JavaScript
Vue学习系列(二)——组件详解
Vue学习系列(二)——组件详解
|
1天前
|
JSON 资源调度 JavaScript
|
2天前
|
分布式计算 资源调度 JavaScript
程序员必知:vue项目创建和启动、ElementUI的安装和快速学习
程序员必知:vue项目创建和启动、ElementUI的安装和快速学习
|
2天前
|
JavaScript
vue基础概念(1)
vue基础概念(1)
6 0
|
JavaScript 测试技术 容器
Vue2+VueRouter2+webpack 构建项目
1). 安装Node环境和npm包管理工具 检测版本 node -v npm -v 图1.png 2). 安装vue-cli(vue脚手架) npm install -g vue-cli --registry=https://registry.
1005 0