Vue组件通信那些事

234 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

前言

在vue的世界里,我们永远绕不开一个话题,那就是组件通信。在vue项目中,我们常常把相同的模块去形成一个组件,一个页面会拆分成多个组件,但是各个组件间需要数据传输,那么就必然涉及到如何传参的问题。在本篇文章中,我们将一起去看看具体有哪些方式能让这些个组件能正常的通信

使用场景

在vue项目中,一个页面是一个树形结构的,因此组件通信的场景也有多种场景,但是大致上不会脱离以下几种:

  1. 父子组件间的通信
  2. 兄弟组件间的通信
  3. 跨辈分组件间的通信
  4. 隔代兄弟组件间的通信

父子组件间通信

示例

我们首先创建一个模块,模块中包含2个组件,分别是parentchild组件。代码大致结构如下:

// parent.vue
<div class="parent-container">
    <child></child>
</div>

那么parent以及child间怎么去通信呢?

props以及emit方式

父向子传参

在vue中我们可以通过使用属性传参的方式将数据进行向下传递,子组件通过props进行接收数据,这样就能达到父向子传递信息的目的,下面我们分别看下parent以及child代码如何实现

// parent.vue
<template>
    <div class="parent-container">
        <Child :msg = 'message'></Child>
    </div>
</template>
<script lang="ts">
import {Vue,Component} from 'vue-property-decorator';
@Component({
    components:{
        Child:()=>import('./child.vue')
    }
})
export default class Parent extends Vue {
    message = '将二锅头传给子组件'

}
</script>
// child.vue
<template>
    <div class="child-container">
        <span>{{msg}}</span>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Prop} from 'vue-property-decorator';
@Component({
    components:{}
})
export default class Child extends Vue {
    @Prop()
    msg:string;
}
</script> 

从上面代码中我们可以看到我们通过属性将msg传递给子组件,子组件通过props进行接收数据,然后展示出来。

子向父传递信息

子组件可以通过vue提供的$emit方法来自定义事件,将数据传递给父组件,由于其是向父组件暴露一个方法回调,因此我们在父组件中需要监听自定义的事件,从而来接收传递的信息。下面我们分别看下parent以及child代码如何实现

// child.vue
<template>
    <div class="child-container">
        <span>{{msg}}</span>
         <Button type="primary" @click="sendMsg"></Button>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Prop, Emit} from 'vue-property-decorator';
@Component({
    components:{}
})
export default class Child extends Vue {
    @Prop()
    msg:string;
    @Emit('on-send-msg')
    public sendMsg() {
        return {
            msg:'将数据传递给父组件'
        }
    }
}
</script>  

child组件中,我们通过定义一个按钮,当点击按钮后,将信息通过$emit方法往父组件暴露,所以父组件需要监听下事件。我们都知道在vue中事件的监听是通过@监听的,因此parent组件实现如下:

<template>
    <div class="parent-container">
        <Child :msg = 'message' @on-send-mag="getMsg"></Child>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Emit} from 'vue-property-decorator';
@Component({
    components:{
        Child:()=>import('./child.vue')
    }
})
export default class Parent extends Vue {
    message = '将二锅头传给子组件'
    public getMsg(data) {
        console.log(data.msg)
    }
}
</script> 

父组件通过@监听在子组件emit出的方法on-send-msg方法。然后将接收到的数据打印在控制台中。这样我们完成了子组件往父组件传递信息。

$parent以及 $children | $refs

依旧采用上述的示例

子组件获取父组件信息

在vue中其底层实现了一些方案,可以让我们能够快速的获取一些组件信息,其中$parent就是其中一个,我们在子组件中可以通过调用此方法获取父组件中所有公共的变量以及公共的方法.注意:如果是定义成private则获取不到。因此子组件的代码可以实现如下:

<template>
    <div class="child-container">
        <span>{{msg}}</span>
         <Button type="primary" @click="showParentMsg"></Button>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Prop, Emit} from 'vue-property-decorator';
@Component({
    components:{}
})
export default class Child extends Vue {
    @Prop()
    msg:string;
    public showParentMsg() {
        console.log(this.$parent.message);
    }
}
</script>  

在上述代码中,我们定义了一个按钮,当我们点击按钮后,会通过$parent将父组件中的message打印出来。因此我们就实现了一种另类的子组件获取父组件参数的方法

父组件获取子组件信息

同上面子组件可以获取父组件信息一样,vue也提供了两种,在父组件中获取子组件信息的办法,分别是$children$refs。我们可以在父组件中直接通过调用$children来获取到父组件中的所有子组件数组,然后去获取每一个子组件中定义的公共方法以及变量,注意:如果是定义成private则获取不到。因此父组件的代码可以实现如下:

<template>
    <div class="parent-container">
        <Child :msg = 'message' @on-send-mag="getMsg"></Child>
        <Button type="primary" @click="getChildInfo">点击获取子组件数据</Button>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Emit} from 'vue-property-decorator';
@Component({
    components:{
        Child:()=>import('./child.vue')
    }
})
export default class Parent extends Vue {
    message = '将二锅头传给子组件'
    public getMsg(data:any) {
        console.log(data.msg)
    }
    public getChildInfo() {
        console.log(this.$children[1].msg);
    }
}
</script> 

在上述代码中,我们定义了一个按钮,当我们点击按钮后,会通过$children将子组件中的msg打印出来。但是我们可以看到,我们获取到的子组件是一个数组,在有些场景下,获取数据就比较麻烦了,因为我们需要去判断那个数据使我们想要的。所幸vue提供一个ref属性,此属性被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的refs中,因此我们可以使用$refs来实现此功能。所以父组件的代码可以实现如下:

<template>
    <div class="parent-container">
        <Child ref="childRef" :msg = 'message' @on-send-mag="getMsg"></Child>
        <Button type="primary" @click="getChildInfo">点击获取子组件数据</Button>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Emit} from 'vue-property-decorator';
@Component({
    components:{
        Child:()=>import('./child.vue')
    }
})
export default class Parent extends Vue {
    message = '将二锅头传给子组件'
    public getMsg(data:any) {
        console.log(data.msg)
    }
    public getChildInfo() {
        console.log(this.$refs.childRef.msg);
    }
}
</script> 

在上述代码中,我们定义了一个按钮,当我们点击按钮后,会通过$refs将子组件中的msg打印出来。这样我们就完成了另一种的组件数据通信

$attr$listeners

此方法仅仅是子组件用来获取父组件信息的方法,且不存在跨层级传递情况,下面我们一一看看

$attr

获取父组件绑定到子组件除classstyle且没有被props接收的属性集合,在子组件中通过this.$attrs来获取。如果还想继续传递给子组件,可以通过v-bind="$attrs"传给其子组件。下面我们来看下代码

// parent.vue
<template>
    <div class="parent-container">
        <Child :msg = 'message' :name = "name"></Child>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Emit} from 'vue-property-decorator';
@Component({
    components:{
        Child:()=>import('./child.vue')
    }
})
export default class Parent extends Vue {
    message = '将二锅头传给子组件';
    name = "二锅头"
}
</script> 
// child.vue
<template>
    <div class="child-container">
        <!-- <span>{{msg}}</span> -->
        <childChild v-bind="$attrs"></childChild>
         <Button type="primary" @click="showParentMsg"></Button>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Prop, Emit} from 'vue-property-decorator';
@Component({
    components:{
        childChild:()=>import('./childChild.vue')
    }
})
export default class Child extends Vue {
    @Prop()
    name?:string;
    test:string="asdsad";
    public showParentMsg() {
        console.log(this.$attrs); // {msg: "将二锅头传给子组件"}
    }
}
</script>  

上面实例定义了一个按钮,点击后获取传入的attrs数据,我们可以看到打印出来的为 {msg: "将二锅头传给子组件"},但是其实我们传入了两个属性,msgname由于name属性已经被props接收了,所以才不会显示

$linteners

获取父组件绑定到子组件除了加.native装饰器的事件,在子组件中通过this.$listeners来获取。如果还想继续传递给子组件,可以通过v-bind="$listeners"传给其子组件。下面我们来看下代码

// parent.vue
<template>
    <div class="parent-container">
        <Child :msg = 'message' @on-send="sendFun" :name = "name" @click.native="clickFun"></Child>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Emit} from 'vue-property-decorator';
@Component({
    components:{
        Child:()=>import('./child.vue')
    }
})
export default class Parent extends Vue {
    message = '将二锅头传给子组件';
    name = "二锅头"
    public sendFun() {
        console.log('发送消息');
    }
    public clickFun() {
         console.log('点击事件');
    }
}
</script> 
// child.vue
<template>
    <div class="child-container">
        <!-- <span>{{msg}}</span> -->
        <childChild v-bind="$attrs"></childChild>
         <Button type="primary" @click="showParentMsg"></Button>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Prop, Emit} from 'vue-property-decorator';
@Component({
    components:{
        childChild:()=>import('./childChild.vue')
    }
})
export default class Child extends Vue {
    @Prop()
    name?:string;
    test:string="asdsad";
    public showParentMsg() {
        console.log(this.$attrs);
        console.log(this.$listeners);//{on-send:Function}
    }
}
</script>  

上面实例定义了一个按钮,点击后获取传入的listeners数据,我们可以看到打印出来的为{on-send:Function},但是其实我们监听了两个事件,on-sendclick由于click事件加.native装饰器了,所以才不会显示

兄弟组件间的通信

借助父组件

我们都知道,在Vue项目中,我们是无法直接从兄弟组件直接将数据进行传递的,那么我们就有了一种最繁琐的一种方案,那就是找一个中介那么我们首先可以想到的中介就是父组件,我们通过将数据不断的传递给父组件,然后在父组件上进行信息的转换即可。这样就能实现兄弟组件间的最繁琐的一种通信,下面我们看下代码。

// parent.vue
<template>
    <div class="parent-container">
        <Child :childMsg = 'childMsg'></Child>
        <Child2  @on-send-msg="child2Msg"></Child2>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Emit} from 'vue-property-decorator';
@Component({
    components:{
        Child:()=>import('./child.vue'),
        Child2:()=>import('./Child2.vue')
    }
})
export default class Parent extends Vue {
    childMsg = ""
    public child2Msg(data:any) {
        debugger;
       this.childMsg = data;
    }
}
</script> 
// child.vue
<template>
    <div class="child-container">
         <span>{{childMsg}}</span>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Prop, Emit} from 'vue-property-decorator';
@Component({
    components:{
        childChild:()=>import('./childChild.vue')
    }
})
export default class Child extends Vue {
    @Prop()
    childMsg?:string;
}
</script>  
// child2.vue
<template>
    <div class="child-container">
       <Button type="primary" @click="sendMsg">点击发送消息</Button>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Prop, Emit} from 'vue-property-decorator';
@Component({
    components:{
        childChild:()=>import('./childChild.vue')
    }
})
export default class Child2 extends Vue {
    @Emit('on-send-msg')
    public sendMsg() {
        return '兄弟,我发送数据给你了'
    }
}
</script>  

在上述例子中,我们定义了两个子组件childchild2组件,当点击child2组件中定义的按钮后,通过$emit方法,将数据传递给父组件,然后通过父组件这个中介来进行数据处理,再通过prop将数据传递给child组件,这样就完成了一种兄弟组件的通信

使用EventBus

EventBus是通过创建一个额外的Vue实例,此实例是一种独立的进程,不同组件间通过借助提供的$on以及$emit方法进行通信,其通信的方式类似广播,也就是说我首先在收音机上设置了电台,当电台要播放东西的时候,就会通知我。下面我们来看下如何创建一个EvnentBus

1: 抽离单独模块

我们可以抽离一个单独的ts或者js文件,然后再需要的地方引用。下面我们看下代码

// bus.ts
import Vue from 'vue'
export const PprBus = new Vue();

然后再需要使用的地方去引用即可

import { PprBus } from './bus';
2: 直接挂载到全局

我们可以在main.ts中,直接通过Vue.propertype,将所需要的EventBus挂载到全局

import Vue from "vue"
Vue.prototype.$PprBus = new Vue();

然后再需要使用的地方世界通过this.$PprBus即可 下面我们看下如何去使用EvnentBus

// child.vue
<template>
  <div class="child-container">
  </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit } from 'vue-property-decorator';
@Component({
  components: {
    childChild: () => import('./childChild.vue')
  }
})
export default class Child extends Vue {
  mounted() {
     (this as any).$PprBus.$on("sendMsg", (data:any) => {
      console.log("这是接收到的数据:", data)
    })
  }
  beforeDestroy(){
        // 取消监听
        (this as any).$PprBus.$off("sendMsg")
    }
}
</script>  
// child2.vue
<template>
    <div class="child-container">
       <Button type="primary" @click="sendMsg">点击发送消息</Button>
    </div>
</template>
<script lang="ts">
import {Vue,Component, Prop, Emit} from 'vue-property-decorator';
@Component({
    components:{
        childChild:()=>import('./childChild.vue')
    }
})
export default class Child2 extends Vue {
    @Emit('on-send-msg')
    public sendMsg() {
         (this as any).$PprBus.$emit("sendMsg", "这是要向外部发送的数据")
    }
}
</script>  

在上述的例子中,我们采用第二种定义EventBus的方法,我们在child中通过this.$PprBus.$on来定义一个需要接听的方法。用来接收数据,然后再child2中通过this.$PprBus.$emit来触发eventBus。如果页面中有多个组件注册了相同的事件,那么所有的组件都能在回调中得到此数据。在前面我们说到了,EventBus是一个独立的进程,因此我们在页面销毁的时候需要将事件去销毁掉,不然会依旧触发

使用Vuex

Vuex是vue提供的一种状态管理器。此管理器将我们定义的所有的组件状态进行统一管理,且进行存储。我们可以借助其提供的API,去进行组件间的通信,由于Vuex篇幅过长,请移步到此查看(后续补充)

跨辈分组件间的通信

跨辈分组件间的通信依旧可以跟兄弟间组件间的通信方式一样,分别是:

  1. 借助中介
  2. 使用vuex 但是Vue又提供了一种额外的方法,进行跨辈分组件间的通信

provide和inject

provideinject这对选项需要一起使用,在祖先组件中我们通过provide注入一个依赖,在子孙组件中可以通过inject来接收注入的依赖。这样我们就脱离了组件层次。无论组件层次有多深,都可以实现

provide

provide选项的数据是一个对象或者是一个返回一个对象的函数。通过此选项,可以将定义的对象或者函数注入到其子孙的property中。

<template>
  <div class="parent-container">
    <Child :childMsg='childMsg'></Child>
  </div>
</template>
<script lang="ts">
import { Vue, Component, Provide } from 'vue-property-decorator';
@Component({
  components: {
    Child: () => import('./child.vue'),
  }
})
export default class Parent extends Vue {
  @Provide()
  info: {
    name: string
  } = {
      name: '来瓶二锅头'
    }
}
</script> 

在上述例子中,我们定义了一个info的属性,然后通过Provide将属性注入到子组件中

inject

在所需要的子孙组件中接收想要添加在这个组件上的数据或方法,数据可以是字符串数组,或者对象,或者一个方法。注意:inject使用的名称必须是provide注入的,否则会保存。下面我们看下上面实例的inject方法

<template>
  <div class="child-container">
      <Button type="primary" @click="clickFun"></Button>
  </div>
</template>
<script lang="ts">
import { Vue, Component, Inject } from 'vue-property-decorator';
@Component
export default class Child extends Vue {
   @Inject()
   info?:{
       name:string
   };
   clickFun () {
       console.log(this.info);
   }
}
</script>  

在上面的例子中,我们点击按钮然后将provide注入的info属性打印出来。 但是我们需要注意一个问题,那就是通过provide和inject传递的数据不是响应式的,因此,如果provide提供的数据改变了,inject已经接收到的数据不会变。基于此原因,个人建议,我们每次通过provide提供一个函数,在inject的时候获取后,在所需要使用的地方直接执行注入的方法即可,下面我们看下代码实现

// parent.vue
export default class Parent extends Vue {
  msg = '二锅头'
  @Provide()
  public sendInfo(){
      return this.msg
  }
}
// child.vue
export default class Child extends Vue {
   @Inject()
   sendInfo?:Function
   clickFun () {
       console.log(this.sendInfo());
   }
}

隔代兄弟组件间通信

隔代兄弟组件间通信依旧可以跟兄弟间组件间的通信方式一样,分别是:

  1. 借助中介
  2. 使用vuex 当然我们也可以结合上面其他的场景,通过融合去处理,但是那样会比较麻烦,因此就不推荐了

结语

至此我们所有的vue2.X中所能涉及到的组件间的通信说明结束,写作不易。喜欢的可以点个关注和点个赞哦。