开发过程中,总会碰到各种情形的组件间通信,然而vue中组件通信的方式有多种,今天进行了小小的总结,接下来就一起来看看有哪些通信方式吧:
一、props / $emit
这个是我们平时用得比较多的方式之一,父组件A通过 props 参数向子组件传递数据,子组件通过 $emit 向父组件发送一个事件(携带参数数据),父组件中监听 $emit 触发的事件得到子组件发送给自己的数据。
话不多说,来个简单例子:
父组件代码:
<template>
<div>
<p>我是父组件</p>
<!-- 通过props把值传给子组件 -->
<son
:text-from-father="text"
@tell-father="tellMe"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import son from "./children.vue"
@Component({
name: 'father',
components: {
son,
}
})
export default class father extends Vue {
text:string = '我是父组件';
执行子组件触发的事件
tellMe(val: string) {
//接受到子组件传过来的值
console.log(val); //I am son
}
}
</script>
子组件代码:
<template>
<div>
<p>我是子组件</p>
<div>父组件告诉我:{
{textFromFather}}</div>
<el-button @click="tellFather">发送文字给父组件</el-button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import {EventBus} from './event-bus.js'
@Component({
name: 'son',
components: {
}
})
export default class son extends Vue {
@Prop({
default: ''
})
textFromFather !: string; //设置props属性值,得到父组件传递过来的数据
tellFather() {
//触发父组件中的事件,向父组件传值
this.$emit('tell-father','I am son')
}
}
</script>
父组件传递了数据给子组件,并且通过v-on绑定了一个tell-father事件来监听子组件的触发事件;
子组件通过props得到相关的text数据,最后通过this.$emit触发了tell-father事件,从而完成通信。
二、$children / $parent
$children 官方介绍:当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。
$parent 官方介绍:当前实例的父实例。
通过$children 拿到的值是数组,是直接儿子的集合,关于具体是第几个儿子,那么儿子里面有个 _uid 属性,可以知道他是第几个元素,是元素的唯一标识符而$parent 是个对象。
既然可以获取到组件的实例,那么就可以调用组件的属性或是方法进行操作。
接下来我们看代码是如何实现通信的:
父组件代码:
<template>
<div>
<p>我是父组件</p>
<children />
<el-button @click="changeChildren">点击改变子组件值</el-button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import children from "./children.vue"
@Component({
name: 'father',
components: {
children
}
})
export default class father extends Vue {
msg: string = '父组件传过来的msg'
changeChildren() {
console.log(this.$children) //所有子组件,打印的是一个数组
//找到children组件,改变属性值
this.$children[0].messageA = 'this is new value from father'
//调用children的方法
this.$children[0].getName('daming'); //my name is daming
}
}
</script>
子组件代码:
<template>
<div>
<p>我是子组件</p>
<p>{
{messageA}}</p>
<p>获取父组件的值为:{
{parentVal}}</p>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component({
name: 'children',
components: {
}
})
export default class father extends Vue {
messageA: string = 'this is old'
get parentVal(){
return this.$parent.msg;//拿到父组件传过来的值
}
getName(name: string) {
console.log(`my name is ${name}`)
}
}
</script>
拿到实例后,父组件可以直接改变子组件的属性,使用子组件的方法。同理,在子组件也可以直接使用父组件的属性和方法。
注:需要注意边界情况,如在#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组。
但是如果父组件需要共享一个属性 share,它的所有子元素都需要访问 share 属性,在这种情况下子组件可以通过 this.$parent.share的方式访问share。
但是,通过这种模式构建出来的组件内部仍然容易出现问题。比如,我们在子组件中嵌套一个子组件children2 ,如:
<template>
<div>
<p>我是父组件</p>
<children>
<children2>
<children/>
</div>
</template>
那么,在children2组件中去访问share时,需要先查看一下,其父组件中是否存在share,如果不存在,则在向上一级查找,落实到代码上为:
var share = this.$parent.share || this.$parent.$parent.share;
这样做,很快组件就会失控:触达父级组件会使应用更难调试和理解,尤其是当变更父组件数据时,过一段时间后,很难找出变更是从哪里发起的。
所以$children和$parent更适合于父子组件通信。
小结:以上两种方式用于父子组件之间的通信,而官方更推荐使用props进行父子组件间通信;
三、ref / refs
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据。
实例:
父组件代码:
<template>
<div>
<p>我是父组件</p>
<son ref="mySon" />
</div>
</template>
<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import son from "./children.vue"
@Component({
name: 'father',
components: {
son
}
})
export default class father extends Vue {
mounted() {
const mySon: any = this.$refs.mySon;
console.log(mySon.name) //lingling
mySon.sayHello(); //hello, I am son
}
}
</script>
子组件代码:
<template>
<div>
<p>我是儿子组件</p>
</div>
</template>
<script lang="ts">
import { Component, Vue, Inject } from "vue-property-decorator";
@Component({
name: 'son',
components: {
}
})
export default class son extends Vue {
name: string = 'lingling'
sayHello () {
console.log('hello, I am son')
}
}
</script>
四、Vuex
Vuex 是一个状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 应用的核心是 store(仓库,一个容器),store 包含着你的应用中大部分的状态 (state);
Vuex各个模块
state:用于数据的存储,是store中的唯一数据源;
getters:如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算;
mutations:类似函数,改变state数据的唯一途径,且不能用于处理异步事件;
actions:类似于mutation,用于提交mutation来改变状态,而不直接变更状态,可以包含任意异步操作;
modules:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护;
Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。
Vuex 解决了多个视图依赖于同一状态和来自不同视图的行为需要变更同一状态的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上。
关于vuex的使用就不举例啦,相信大家都很熟悉了。如果还是有疑问,那就移步官方文档走一遭:https://vuex.vuejs.org/zh/
五、provide / inject
概念:provide / inject这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
简单来说就是父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。
provide 选项应该是:一个对象或返回一个对象的函数;
inject 选项应该是:一个字符串数组,或 一个对象,对象的 key 是本地的绑定名;
接下来看例子:
现在有三个组件: father.vue sun.vue grandson.vue 其中sun.vue是father.vue的子组件,grandson.vue 是sun.vue的子组件。
父组件代码:
<template>
<div>
<p>我是父组件</p>
<son />
</div>
</template>
<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import son from "./children.vue"
@Component({
name: 'father',
components: {
son
}
})
export default class father extends Vue {
@Provide() text = "I am father"
}
</script>
儿子组件代码:
<template>
<div>
<p>我是儿子组件</p>
<grandson />
</div>
</template>
<script lang="ts">
import { Component, Vue, Inject } from "vue-property-decorator";
import grandson from "./grandson.vue"
@Component({
name: 'son',
components: {
grandson
}
})
export default class father extends Vue {
@Inject() readonly text!: string
mounted () {
console.log(`儿子组件拿到了:${this.text}`) //儿子组件拿到了:I am father
}
}
</script>
孙子组件代码:
<template>
<div>
<p>我是孙子组件</p>
</div>
</template>
<script lang="ts">
import { Component, Vue, Inject} from "vue-property-decorator";
@Component({
name: 'grandson',
components: {
}
})
export default class father extends Vue {
@Inject() readonly text!: string
mounted () {
console.log(`孙子组件也拿到了:${this.text}`) //孙子组件也拿到了:I am father
}
}
</script>
父组件提供了text属性,在它的儿子和孙子组件里都能取到这个属性,层级更深也是可以直接拿到的,可以不需要一层一层进行传递了。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:
- 祖先组件不需要知道哪些后代组件使用它提供的属性
- 后代组件不需要知道被注入的属性来自哪里
然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。
六、eventBus
eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。
这种方法通过一个空的vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。
具体实现方式:
const EventBus=new Vue();
Event.$emit(事件名,数据);
Event.$on(事件名,data => {});
接下来我们看例子:
现在父组件有两个组件: children.vue 和 children2.vue, 接下来我们来看这两个组件是如何通过eventBus进行通信的
首先,需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它。
初始化:event-bus.js
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
父组件代码:
<template>
<div>
<p>我是父组件</p>
<son />
<son2 />
</div>
</template>
<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import son from "./children.vue"
import son2 from "./children2.vue"
@Component({
name: 'father',
components: {
son,
son2
}
})
export default class father extends Vue {
}
</script>
子组件1代码:
// 在儿子1组件中发送事件
<template>
<div>
<p>我是儿子1组件</p>
<el-button @click="addNumber">+加法器</el-button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import {EventBus} from './event-bus.js'
@Component({
name: 'son',
components: {
}
})
export default class son extends Vue {
num: number = 1
addNumber() {
EventBus.$emit('addition', {
num:this.num++
})
}
}
</script>
子组件2代码:
//儿子2组件中接收事件
<template>
<div>
<p>我是儿子2组件</p>
<div>计算所得结果: {
{count}}</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import {EventBus} from './event-bus.js'
@Component({
name: 'son2',
components: {
}
})
export default class son extends Vue {
count: number = 0;
mounted() {
EventBus.$on('addition', (param: any) => {
this.count = this.count + param.num;
})
}
}
</script>
这样就实现了在组件children.vue中点击加法器按钮,在children2.vue中利用传递过来的num展示求和的结果。
eventBus让组件间通信变得简单,不过同时也有它的不足:就是当项目较大,就容易造成难以维护的灾难。
七、$attrs与 $listeners
$attrs:包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。
$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。
现在我们来讨论一种情况, 现在父组件和孙子组件是隔代关系, 那它们之前进行通信有哪些方式呢?
- 使用props绑定来进行一级一级的信息传递, 如果孙子组件中状态改变需要传递数据给父组件, 使用事件系统一级级往上传递;
- 使用eventBus,这种情况下还是比较适合使用, 但是碰到多人合作开发时, 代码维护性较低, 可读性也低;
- 使用vuex来进行数据管理, 但是如果仅仅是传递数据, 而不做中间处理,使用vuex处理感觉有点大材小用了。
接下来看例子:
父组件代码:
<template>
<div>
<p>我是父组件</p>
//这里我们可以看到,父组件向儿子组件传递了四个参数和一个方法
<son
:name="name"
:age="age"
:gender="gender"
title="我是父组件传过来的title"
v-on:upText="clickText"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import son from "./children.vue"
@Component({
name: 'father',
components: {
son
}
})
export default class father extends Vue {
name: string = 'fatherName';
age: number = 45;
gender: string = '男'
clickText(val: string) {
console.log(`当前val是:${val}`); //子组件触发打印:当前val是:I am son //孙子组件触发打印:当前val是:I am grandson
}
}
</script>
儿子组件代码:
<template>
<div>
<p>我是儿子组件</p>
<p>儿子组件的$attrs: {
{ $attrs }}</p>
<el-button @click="tellFather">点击传值给父组件</el-button>
<grandson v-bind="$attrs" v-on="$listeners" />
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import grandson from "./grandson.vue"
@Component({
name: 'son',
components: {
grandson
}
})
export default class son extends Vue {
@Prop({
default: '男'
})
gender!: string; //性别作为props属性绑定
created() {
// 这里的$attrs就是所有从父组件传递过来的所有参数 然后 除去props中显式定义的参数后剩下的所有参数!!!
console.log(this.$attrs,'attrs'); //age:45 name:fatherName title:我是父组件传过来的title
console.log(this.$listeners,'listeners') //upText:function
}
tellFather() {
this.$emit('upText','I am son')
}
}
</script>
孙子组件代码:
<template>
<div>
<p>我是孙子组件</p>
<p>孙子组件的$attrs: {
{ $attrs }}</p>
<el-button @click="tellGrandFather">点击传值给爷爷组件</el-button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop} from "vue-property-decorator";
@Component({
name: 'grandson',
components: {
}
})
export default class grandson extends Vue {
@Prop({
default: '12'
})
age!: string; //子组件传过来的age作为props被绑定
created() {
// 同理,$attrs这个对象集合中的值 = 所有传值过来的参数 - props中显示定义的参数
console.log(this.$attrs,'孙子组件的attrs'); //name: fatherName title:我是父组件传过来的title
console.log(this.$listeners,'孙子组件的listeners'); // upText:function
}
tellGrandFather() {
this.$emit('upText','I am grandson'); //触发upText事件,直接进行跨级通信
}
}
</script>
总的来说:$attrs与$listeners 是两个对象,$attrs 里存放的是父组件中绑定的非 Props 属性,$listeners里存放的是父组件中绑定的非原生事件。通过$attrs、$listeners 则可以很方便的进行一些跨级通信。
写在最后的总结:
按照常见的使用场景归类大致可以分为三类:
- 父子组件间通信:props; $parent / $children; ref ; provide / inject ; $attrs / $listeners
- 兄弟组件间通信:eventBus ; vuex;
- 跨级通信:vuex; eventBus;provide / inject ; $attrs / $listeners
组件间进行通信的方式有多种,在合适的场景选择不同的通信方式,也会给开发带来极大的便利。