前言
组件式开发作为 vue 框架核心思想之一,在我们利用 vue 框架做前端开发时,避免不了的要与之打交道。在实际开发中,vue 组件实例间的作用域是相互独立的,但是组件间却是需要相互通信的,共享状态等。所以掌握 vue 组件间何如通信,就显得十分重要,以下总结一下 vue 的 N 种通信,以便回顾和巩固一下知识。
组件间的关系
说到组件间的通信,就有必要了解一下组件间存在哪些关系。 (网上盗图)

- 父子:A组件 -> B组件
- 非父子: 隔代 A组件 -> C组件;兄弟 B组件 -> C组件等
组件间通信的方式
介绍完组件间的关系,可以先来看下 vue 中有哪些通信方式
- v-bind 和 props (通过绑定属性进行传值)
- v-on 和 $emit (通过触发事件进行传值)
- $ref、$parent、$children(通过获取到dom进行传值)
- provide和inject (使用依赖注入进行传值)
- $attrs 和 $listeners (获取剩余参数进行传值)
- EventBus (利用事件总线进行传值)
- vuex (利用 vuex 插件进行传值)
- 利用本地存储和vue-router等方式
组件间通信方式详解
父子组件间的通信
以下实例均使用vue-cli3 创建的vue工程进行测试
// App.vue 代码如下,后续不在贴出
<template>
<div id="app">
<parent></parent>
</div>
</template>
<script>
import Parent from './components/parent';
export default {
name: 'App',
components: {
Parent
}
};
</script>
父组件向子组件传值(v-bind 和 props)
// parent 父组件
<template>
<div>
<h1>父组件</h1>
// 在引入的子组件上,通过v-bind绑定一个属性Pmsg,值为msg
<children :Pmsg="msg"></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '来自父组件的消息'
};
}
};
</script>
// children 子组件
<template>
<div class="children">
<h2>子节点</h2>
<p>{{ Pmsg }}</p>
</div>
</template>
<script>
export default {
name: 'children',
// 通过props接收在父组件中绑定的属性,然后就可以直接在内部中使用了。
// 顺带提一句 props可以以数组的方式接收属性,也可以以对象的方式接收
props: ['Pmsg'],
// props: {
// Pmsg: {
// type: String // 接收的类型,
// default:any,
// required:Boolean,
// validator:Function,
// }
// },
mounted() {
console.log(this.Pmsg);
}
};
</script>
在页面中查看,发现子组件已经接收到父组件传入的值

子组件向父组件传值(v-on 和 $emit)
// parent 父组件
<template>
<div>
<h1>父组件</h1>
<p>{{ msg }}</p>
// 在子组件标签中添加事件监听,等待子组件触发
// 注意!监听的是子组件的\$emit('receiveData'),在父组件中触发的是parnetReceiveData
<children @receiveData="parnetReceiveData"></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '父组件原来的值'
};
},
methods: {
parnetReceiveData(data) {
console.log(data);
this.msg = data;
}
}
};
</script>
// children 子组件
<template>
<div class="children">
<h2>子节点</h2>
<p @click="emitData">{{ msg }}</p>
</div>
</template>
<script>
export default {
name: 'children',
data() {
return {
msg: '子组件的值'
};
},
methods: {
emitData() {
// 触发在父组件中,子组件标签上监听的receiveData事件,
// 从而触发父组件事件,利用参数传递,达到传递值得效果。
this.$emit('receiveData', this.msg);
}
}
};
</script>
在页面中查看效果,启动页面,点击后触发$emit事件,父组件成功接收到子组件传来的值

$parnet 和 $children、$ref
// parent 父组件
<template>
<div>
<h1>父组件</h1>
<p @click="emitData">{{ msg }}</p>
<children></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '父组件原来的值'
};
},
methods: {
emitData() {
// 直接获取到vdom,然后修改值
console.log('praent的子组件', this.$children);
this.$children[0].msg = '父组件通过dom方式传过来的值';
}
}
};
</script>
// children 子组件
<template>
<div class="children">
<h2>子节点</h2>
<p @click="emitData">{{ msg }}</p>
</div>
</template>
<script>
export default {
name: 'children',
data() {
return {
msg: '子组件的值'
};
},
methods: {
emitData() {
// 直接获取到vdom,然后修改值
console.log('children的父组件', this.$parent);
this.$parent.msg = '子组件通过dom方式传过来的值';
}
}
};
</script>
在页面中验证效果,点击父组件,可以看到子组件的值被修改了,同时注意看 $children ,为一个数组需要注意的是 $children 数组的顺序不一定是对的,取决于子组件什么时候插入到父组件的,这点需要特别注意。


利用ref
// parent 父组件
<template>
<div>
<h1 ref="h1">父组件</h1>
<p @click="changeData">{{ msg }}</p>
<children ref="children"></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '父组件原来的值'
};
},
methods: {
changeData() {
// 答应$refs 和 通过ref获取到vnode,直接修改值
console.log(this.$refs);
this.$refs['children'].msg = '通过ref修改的值';
}
}
};
</script>
// children 子组件
<template>
<div class="children">
// 在子组件中使用ref标识 h2 标签
<h2 ref="h2">子节点</h2>
<p @click="showRoot">{{ msg }}</p>
</div>
</template>
<script>
export default {
name: 'children',
data() {
return {
msg: '子组件原来的值'
};
},
methods: {
showRoot() {
// 打印\$root
console.log(this.$root);
}
}
};
</script>
页面中点击父组件,控制台可以看到父组件的refs对象,分别保存了父组件中通过ref标识的两个vnode,通过向下查找可以看到在子组件的vnode中同样存在子组件中标识的ref


小拓展
介绍完前面三种,父子组件间传值就基本介绍完毕。 通过了解了 props 和 $emit 两种方式,就可以顺带扩展一下 v-model 这个api 和 .sync 修饰符
- v-model 是一个语法糖,其实质是通过 props 和 $emit 来达到数据双向绑定的效果的 在input中
// parnet 父组件
<template>
<div>
<h1>父组件</h1>
<p>{{ msg }}</p>
// 利用v-model达到数据双向绑定的效果
<input type="text" v-model="msg">
</div>
</template>
<script>
export default {
name: 'praent',
data() {
return {
msg: '父组件原来的值'
};
},
};
</script>
在页面中的input框中输入或者修改值,p标签也会同时被修改

// parent 父组件
<template>
<div>
<h1>父组件</h1>
<p>{{ msg }}</p>
<input
type="text"
:value="msg"
// 监听input,获取事件的值的同时赋值给msg
@input="msg = $event.target.value"
>
</div>
</template>
<script>
export default {
name: 'praent',
data() {
return {
msg: '父组件原来的值'
};
},
};
</script>
是可以得到同样的效果,有兴趣的可以是试一下
但是,在 vue 中, v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
- text 和 textarea 元素使用 value property 和 input 事件;
- checkbox 和 radio 使用 checked property 和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
这些均可以在 vue 的的官方帮助文档上找到,以及自定义组件的 v-model 自定义事件的使用详情
- .sync 修饰符 先看 .sync修饰符的使用方式
// parent 父组件
<template>
<div>
<h1>父组件</h1>
<p>{{ msg }}</p>
// 在传递的数据中,添加.sync修饰符
<children :msg.sync="msg"></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '父组件原来的值'
};
}
};
</script>
// children 子组件
<template>
<div class="children">
<h2>子节点</h2>
<p @click="syncData">{{ msg }}</p>
</div>
</template>
<script>
export default {
name: 'children',
props: ['msg'],
methods: {
syncData() {
// 这里需要 \$emit 触发update:msg 事件,并且把要修改的值进行传递
this.$emit('update:msg', '通过sync方式修改msg值');
}
}
};
</script>
启动页面,在页面中点击子组件的值

// .sync 修饰符其实就是将以下形式做成了一个语法糖
<template>
<div>
<h1>父组件</h1>
<p>{{ msg }}</p>
// 就是对@update:msg 改为.sync
<children
:msg="msg"
@update:msg="msg = $event"
></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '父组件原来的值'
};
}
};
</script>
非父子组件传值
provide和inject
provide 和 inject 适用于隔代传值,爷孙或者更深的层级。
官方说明:这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
同时需要注意官网中的该条提示:
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
也就是说,Vue 并不会对 provide 中的变量进行响应式处理。但是如果我传递的变量已经是经过响应式处理的 inject 接受的变量也会是响应式的。 以下就通过代码来说明 先介绍以下两个api的使用
// provide
// 1. 可以使用工厂函数返回一个对象,使用该方法就可以获取到this
provide: function () {
return {
msg: this.msg
}
}
// 2. 可以直接为一个对象
provide: {
msg: '传入的值'
}
// inject
// 1. 接收一个数组
inject: ['msg']
// 2. 接收一个对象
inject: {
msg: { // 当然msg的值也可以直接直接为字符串
from: 'msg' // 注入内容中的 key
default: '' // 默认值,也可以是个工厂函数
}
}
非响应式的数据传值代码演示
// parent 父组件
<template>
<div>
<h1 ref="h1">父组件</h1>
<p @click="changeData">{{ msg }}</p>
<children ref="children"></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '来自父亲的值'
};
},
methods: {
changeData() {
this.msg = '父组件改变了向下传递的msg值';
}
},
provide() {
return {
msg: this.msg
};
}
};
</script>
// children 子组件
<template>
<div class="children">
<h2 ref="h2">子节点</h2>
<p>{{ msg }}</p>
<grandson></grandson>
</div>
</template>
<script>
import Grandson from './grandson';
export default {
name: 'children',
components: {
Grandson
},
inject: ['msg']
};
</script>
启动页面,点击父组件p标签,父组件的值已经修改了,向下传递的值并没有修改

如果传入的值为响应式的
<template>
<div>
<h1 ref="h1">父组件</h1>
<p @click="changeData">{{ msg.value }}</p>
<children ref="children"></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
// 稍微把msg的值改为一个对象
msg: { value: '来自父亲的值' }
};
},
methods: {
changeData() {
this.msg.value = '父组件改变了向下传递的msg值';
}
},
provide() {
return {
msg: this.msg
};
}
};
</script>
// children 子组件
<template>
<div class="children">
<h2 ref="h2">子节点</h2>
<p>{{ msg.value }}</p>
<grandson></grandson>
</div>
</template>
<script>
import Grandson from './grandson';
export default {
name: 'children',
components: {
Grandson
},
inject: ['msg']
};
</script>
// grandson 孙组件
<template>
<div class="grandson">
<h3>孙组件</h3>
<p>{{ msg.value }}</p>
</div>
</template>
<script>
export default {
name: 'grandson'
inject: ['msg']
};
</script>
启动页面,此时再去点击父组件的p标签,现在不单单父组件的值更新了,连同子组件和孙组件的值都发生变化了

将孙组件稍微修改一下,定义一个值去存储inject接受到的响应式的值,再通过点击事件去修改值,
<template>
<div class="grandson">
<h3>孙组件</h3>
<p @click="changeMsg">{{ grandsonMsg.value }}</p>
</div>
</template>
<script>
export default {
name: 'grandson',
data() {
return {
grandsonMsg: ''
}
},
mounted(){
this.grandsonMsg = this.msg
},
inject: ['msg'],
methods:{
changeMsg(){
this.grandsonMsg.value = '孙组件修改了msg值'
}
}
};
</script>
可以看到,孙组件接受到这个响应式的值,然后通过这种的方式去修改这个值,同样可以修改到父组件provide下来的值

provide 和 inject 大量存在于高阶组件中,如element-ui中form表单,button按钮等,均使用了这两个api


$attrs 和 $listeners
官方文档中对$attrs的说明
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
也就是说所接受到的属性是不包含class、style、和已经被prop获取的到的,等下在代码中可以注意一下。直接看代码 同时注意一下子组件中的配置项inheritAttrs。
官方文档对inheritAttrs配置项的说明
默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例 property $attrs 可以让这些 attribute 生效,且可以通过 v-bind 显性的绑定到非根元素上
// parent 父组件
<template>
<div>
<h1 ref="h1">父组件</h1>
<p>{{ msg }}</p>
<children
:msgToChildren="msgToChildren"
:msgToGrandson="msgToGrandson"
></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msgToChildren: '父亲的值传给孩子的值',
msgToGrandson: '父亲的值传给孙子的值'
};
}
};
</script>
// 子组件
<template>
<div class="children">
<h2 ref="h2">子节点</h2>
<p>{{ this.msgToChildren }}</p>
<grandson v-bind="$attrs"></grandson>
</div>
</template>
<script>
import Grandson from './grandson';
export default {
name: 'children',
//
// inheritAttrs: false,
components: {
Grandson
},
props: ['msgToChildren']
};
</script>
// grandson 孙组件
<template>
<div class="grandson">
<h3>孙组件</h3>
<p>{{ msgToGrandson }}</p>
</div>
</template>
<script>
export default {
name: 'grandson',
props: ['msgToGrandson']
};
</script>
运行页面,孙组件可以拿到父组件传下去的值,同时注意子组件标签上的属性,msgToGrandson依旧存在

将子组件中inheritAttrs配置项这是为false
// children 子组件
<template>
<div class="children">
<h2 ref="h2">子节点</h2>
<p>{{ this.msgToChildren }}</p>
<grandson v-bind="$attrs"></grandson>
</div>
</template>
<script>
import Grandson from './grandson';
export default {
name: 'children',
inheritAttrs: false,
components: {
Grandson
},
props: ['msgToChildren']
};
</script>
再看到页面,此时子组件上的msgToGrandson的属性就被抛弃了

看完$attrs的使用,再来看$listeners的使用
官方文档对 $listrners的说明
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
注意的是$listeners 可以监听到所有的事件,直接看代码实现
<template>
<div>
<h1 ref="h1">父组件</h1>
<p @click="changrMsg">{{ msg }}</p>
<children
:msgToChildren="msgToChildren"
:msgToGrandson="msgToGrandson"
@changeFromChildren="changeFromChildren"
@changeFromGrandson="changeFromGrandson"
></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '重置父亲的值',
msgToChildren: '父亲的值传给孩子的值',
msgToGrandson: '父亲的值传给孙子的值'
};
},
methods: {
changrMsg() {
console.log('重置msg');
this.msgToChildren = '父亲的值传给孩子的值';
this.msgToGrandson = '父亲的值传给孙子的值';
},
changeFromChildren() {
console.log('触发父组件changeFromChildren事件');
this.msgToChildren = '子组件改变了msg值';
},
changeFromGrandson() {
console.log('触发父组件changeFromGrandson事件');
this.msgToGrandson = '孙组件改变了msg值';
}
},
provide() {
return {
msg: this.msg
};
}
};
</script>
<template>
<div class="children">
<h2 ref="h2">子节点</h2>
<p @click="changeMsg">{{ this.msgToChildren }}</p>
<grandson
v-bind="$attrs"
v-on="$listeners"
></grandson>
</div>
</template>
<script>
import Grandson from './grandson';
export default {
name: 'children',
inheritAttrs: false,
components: {
Grandson
},
props: ['msgToChildren'],
methods: {
changeMsg() {
this.$emit('changeFromChildren');
}
}
};
</script>
<template>
<div class="grandson">
<h3>孙组件</h3>
<p @click="changeMsg">{{ msgToGrandson }}</p>
<p @click="changeChildrenMsg">修改子组件</p>
</div>
</template>
<script>
export default {
name: 'grandson',
props: ['msgToGrandson'],
methods: {
changeMsg() {
this.$emit('changeFromGrandson');
},
changeChildrenMsg() {
this.$emit('changeFromChildren');
}
}
};
</script>

小总结
跨层级的组件通信也基本介绍完毕,一般而言$attrs 和 $listeners 使用于爷孙组件之间的通信,如果层级太深,需要再每一层中绑定$attrs 和 $listeners 属性,此时就应该考虑使用其他方式,而provide和inject使用于祖辈向其子孙传递值,再一定场景下,可以利用其来实现兄弟组件的状态共享。 介绍完这几种方式其实vue提供的组件通信方式也基本就在这里了,然后我们可以发现,vue确实是没有提供直接用于兄弟组件间通信的方式。那么兄弟组件间通信我们可以实现呢?
兄弟组件通信
由于vue并没有提供直接通信的方式,一般需要我们使用其他方式进行通信。
EventBus 事件总线
事件总线的原理其实就是使用发布订阅模式,创建一个中介,A组件把需要传递的值或者触发的事件告知中介,由中介去告诉B组件。 先简单实现一个发布订阅
class Eventbus {
constructor() {
this.list = {};
}
$on(type, fn) {
if (this.list[type]) {
this.list[type].push(fn);
} else {
this.list[type] = [];
this.list[type].push(fn);
}
}
$emit(type) {
if (this.list[type]) {
this.list[type].forEach(fn => {
fn();
});
}
}
}
const eventBus = new Eventbus();
export default eventBus;
在父组件、第一组件加载完成时添加一个监听事件
// parent 父组件
<template>
<div>
<h1 ref="h1">父组件</h1>
<p>{{msg}}</p>
<first></first>
<second></second>
</div>
</template>
<script>
import First from './first';
import Second from './second';
import eventBus from './eventBus';
export default {
name: 'praent',
components: {
First,
Second
},
data() {
return {
msg: '父亲的值'
};
},
mounted() {
eventBus.$on('aaa', () => {
console.log('父组件的msg被修改了');
this.msg = '父组件的msg被修改了';
});
}
};
</script>
// first第一组件
<template>
<div class="first">
<h3>第一兄弟组件</h3>
<p>{{ msg }}</p>
</div>
</template>
<script>
import eventBus from './eventBus';
export default {
name: 'first',
data() {
return {
msg: '第一兄弟组件'
};
},
mounted() {
eventBus.$on('aaa', () => {
console.log('第一兄弟组件的msg被修改了');
this.msg = '第一兄弟组件的msg被修改了';
});
}
};
</script>
在第二组件中添加一个事件,该事件触发时会去触发定义在eventBus中的事件
<template>
<div class="second">
<h3>第二兄弟组件</h3>
<p @click="emitEventBus">第二兄弟组件</p>
</div>
</template>
<script>
import eventBus from './eventBus';
export default {
name: 'second',
methods: {
emitEventBus() {
console.log('第二兄弟组件触发事件');
eventBus.$emit('aaa');
}
}
};
</script>
当我们点击第二组件时,可以看到,无论时父子组件,还是兄弟组件,只要是$on 监听了eventBus中的事件,只要在任何地方通过eventBus $emit该事件,就可以执行。

当然在实际开发中,vue 实例内部本身已经实现了事件的监听和触发,我们可以利用vue实例来做属性和事件的监听,并不需要我们自己去实现一个发布订阅
我们可以将eventBus文件修改下
import Vue from 'vue'
const eventBus = new Vue()
export default eventBus;
可以自行测试一下,同样是可以使用的
同时,我们还可以在main.js中将event挂载到vue根实例的prototype上,这样就不用再每次需要使用eventBus 都要去引用
// main.js
import Vue from 'vue'
import App from './App.vue'
import eventBus from './components/eventBus';
Vue.prototypt.$eventBus = eventBus
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
添加监听和emit事件只需要修改一下代码即可。使用起来就方便很多
this.$eventBus.$on('aaa', () => {
console.log('第一兄弟组件的msg被修改了');
this.msg = '第一兄弟组件的msg被修改了';
});
this.$eventBus.$emit('aaa');
vuex
剩下最后一个vueX,可以直接阅读官方文档 vuex 查看他的使用,
后续有时间就单独写一篇。