作为Vue的核心,组件化为我们节省了大量的开发工作。页面作为组件的容器,在里面我们可以随意的嵌套,组合使用组件。但每个组件都是一个封闭的空间,在一个页面中,我们常常需要做到各组件之间的联动,这个时候我们就要进行组件之间的通信。我们通常根据组件之间的关系来使用不同的通信方式,如上图所示,组件之间的关系通常有如下几种:
父子组件通信
如图中所示的父组件A和子组件B,C的关系,就是父子组件关系,也是我们开发中最常遇到的组件关系。这种情况下我们可以用以下方法进行通信:
1. props/$emit
父组件通过 v-bind:key="xxx" / :key="xxx"的方式向子组件传参,子组件用props接收传入的参数。子组件通过$emit向父组件传递事件,父组件则通过 v-on:event="myEvent" / @event="myEvent" 来监听子组件的事件。下面我们通过一个例子来展示具体的使用:
//父组件 Parent.vue
<template>
<div id="app">
<children :names="names" @submit="submit"></children>
</div>
</template>
<script>
import children from "./components/children"
export default {
components:{
"users":Users
},
data(){
return{
names:["Henry","Bucky","Emily"]
}
},
methods: {
submit(data) {
console.log(data)
}
}
}
//children子组件
<template>
<div class="hello">
<ul>
<li v-for="name in names">{{name}}</li>//遍历传递过来的值,然后呈现到页面
</ul>
<button @click="submitCount"></button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props:{
names:{ //这个就是父组件中子标签自定义名字
type:Array,
required:true
}
},
methods: {
submitCount() {
this.$emit('submit', names.length) //此外submit为父组件中子标签处监听的事件名
}
}
}
</script>
2.$parent / $children与 ref
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent/$children:访问父 / 子实例 这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。我们先来看个用ref来访问组件的例子:
// component-a 子组件
export default {
data () {
return {
title: 'Vue.js'
}
},
methods: {
sayHello () {
window.alert('Hello');
}
}
}
// 父组件
<template>
<component-a ref="comA"></component-a>
</template>
<script>
export default {
mounted () {
const comA = this.$refs.comA;
console.log(comA.title); // Vue.js 访问对应组件实例的数据
comA.sayHello(); // 弹窗 调用对应组件实例的方法
}
}
</script>
$children 可以用来在父组件中访问子组件,但需要注意的是, this.$children拿到的是一个组件实例的数组,而且是无序的,也不是响应式的。这样导致我们无法明确的找到指定的子组件,所以不推荐使用。
$parent 当前组件树的一级父实例。如果当前实例没有父实例,此实例将会是其自己。
兄弟组件通信
图中的子组件B和子组件C就是我们说的兄弟组件,它们有一个共同的父组件。因为有了这个共同父组件桥梁的存在,我们可以通过上面介绍的 props/$emit 和 $ref组合来实现兄弟组件间的通信:
//子组件B
this.$emit('submit', data) //通过$emit通知到父组件
//父组件A
<children-b @submit="submit"></children-b> //监听B组件的提交事件
<children-c ref="child_c"></children-c>
...
methods: {
submit(data) {
this.$refs.child_c.acceptBmsg(data) //通过refs拿到子组件C实例,直接调用C组件方法接收从B传过的数据
}
}
//子组件C
methods: {
//用于接收B组件传来的数据
acceptBmsg(data) {
console.log(data)
}
}
虽然这样也可以实现兄弟组件间的通信,但毕竟过了一道父组件的转发,需要在父组件中添加一些转发的逻辑代码。如果项目中这种情况较多的情况下,我们就需要在每个父组件中都得写转发逻辑,这样会导致代码混乱。这时我们就可以通过EventBus(又叫:事件总线)来统一管理。
3.EventBus
这种方法通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、隔代、陌生组件。这里其实有点像发布,订阅者模式。可以做到一个组件发布,多个组件订阅;多个组件发布,一个组件订阅;多个组件发布,多个组件订阅。实现如下:
在目录下新建bus.js文件,新建并输出一个空的Vue实例
//bus.js
import Vue from 'vue'
const bus = new Vue();
export default bus
场景: A组件发信息给B组件
//A组件
<template>
<div>
<button @click="send">将数据发送给B组件</button>
</div>
</template>
<script>
import { EventBus } from './bus.js'
export default {
data(){
return {
name: 'LiLei'
}
},
methods: {
send(){
EventBus.$emit('sengMsg', this.name)
}
}
}
</script>
//B组件
<script>
import { EventBus } from './bus.js'
export default {
mounted(){
EventBus.$on('sengMsg',(data)=>{
console.log(data)
})
}
}
</script>
袓孙(隔代)组件通信
在我们的实际开发中,还会遇到另外一种情况即多层嵌套的组件,如开头图中的组件A和组件D的关系,通常情况下,父组件向孙组件传递数据,可以采用父子props层层传递,也可以使用EventBus直接交互。在Vue2.2.0之后,Vue还提供了provide/inject选项。
这对选项允许一个祖先组件向其所有子孙后代组件注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。 也就是说,在父组件只要声明了
provide,在其子组件,孙组件,曾孙组件等能形成上下游关系的组件中交互,无论多深都能通过inject来访问provider中的数据。而不是局限于只能从当前父组件的prop属性来获取。注意他只做祖先通后代的单向传递的一个办法。有人这么形容:
provide就相当于加强版父组件prop,可以跨越中间组件,inject就相当于加强版子组件的props
使用方法:
- provide 提供变量:
Object | () => Object - inject 注入变量:
Array<string> | { [key: string]: string | Symbol | Object }
provide 选项应该是一个对象或返回一个对象的函数。 该对象包含可注入其子孙的属性。在该对象中,它支持ES6中Symbol作为key,但只在原生支持等环境下可工作。
inject 选项可以是
-
一个字符串数组
-
一个对象,
key为本地绑定名,value为:- 在可用的注入内容中搜索用的
key,或 - 一个对象,其
from属性是在可用的注入内容中搜索用的key,default属性是降级情况下使用的value
- 在可用的注入内容中搜索用的
提示:
provide和inject绑定并不是可响应的,这是刻意为之,然而如果你传入了一个可监听的对象,那么该对象的属性还是可监听的。(使用中我们常传入整个组件实例)
可以使用Vue 2.6最新API Vue.observable 优化响应式 provide(推荐)
//父组件
<template>
<div>
<p>{{ title }}</p >
<son></son>
</div>
</template>
<script>
import Son from "./son"
export default {
name: 'Father',
components: { Son },
// provide选项提供变量
provide: {
message: 'provided by father'
},
data () {
return {
title: '父组件'
}
},
methods: { ... }
}
</script>
我们在子组件中不做任何处理
//子组件
<template>
<div>
<p>{{ title }}</p >
<grand-son></grand-son>
</div>
</template>
<script>
import grandSon from "./grandSon "
export default {
name: "Son",
components: { grandSon },
data () {
return {
title: '子组件'
}
},
};
</script>
在孙子组件中,我们通过inject来接收父组件中provide中的数据
//孙子组件
<template>
<div>
<p>message:{{ message }}</p >
</div>
</template>
<script>
export default {
name: "GrandSon",
inject: [ "message" ],
data () {
return {
title: '孙组件'
}
},
methods: { ... }
};
</script>
陌生组件通信
上面所有的情况都是有血缘关系的组件通信,那对于毫无血缘关系的组件,我们该采用哪种通信方式呢?其实上面提到的EventBus已经可以满足陌生组件间的通信。但同时Vue也提供了更加强大的Vuex(状态管理模式)来实现多个组件的状态共享。
Vuex成员列表:
- state 存放状态
- mutations state成员操作
- getters 加工state成员给外界
- actions 异步操作
- modules 模块化状态管理
简要介绍各模块在流程中的功能:
- Vue Components:Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
- state:页面状态管理容器对象。集中存储Vue components中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
- getters:state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。
- dispatch:操作行为触发方法,是唯一能执行action的方法。
- actions:操作行为处理模块,由组件中的
$store.dispatch('action 名称', data1)来触发。然后由commit()来触发mutation的调用 , 间接更新 state。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。 - commit:状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
- mutations:状态改变操作方法,由actions中的
commit('mutation 名称')来触发。是Vuex修改state的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
如果想系统地学习Vuex的使用,请参考:# Vuex 是什么
总结:
我们上面介绍了多种组件间通信的方式,但除了学会如何使用,更重要的是我们要明白什么场景下适合用哪种通信方式。如果你的项目只是一个简单的应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式 (opens new window)就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。条条大路通罗马,但最重要的是你要找到那条最近,最省力的那条。
莎士比亚说过, 看完不点赞的同志,不是好同志