Vue 组件通信
作者:Seiya
时间:2019年08月27日
组件通信
组件通信一般分为以下几种情况:
父子组件通信
兄弟组件通信
跨多层级组件通信
任意组件
对于以上每种情况都有多种方式去实现,接下来就来学习下如何实现。
父组件通信
方式一:props、emit
父组件通过 props 传递数据给子组件,子组件通过 emit 发送事件传递数据给父组件,这两种方式是最常用的父子通信实现办法。
方式二:v-model
v-model 本质上是一种语法糖,在《Vue 单向数据流和双向绑定》一文中已有介绍。
方式三:$listeners、.sync
$listeners 属性会将父组件中的 (不含 .native 修饰器的) v-on 事件监听器传递给子组件,子组件可以通过访问 $listeners 来自定义监听器。
兄弟组件通信
对于这种情况可以通过查找父组件中的子组件实现,也就是 this.children,在 $children 中可以通过组件 name 查询到需要的组件实例,然后进行通信。
当然,也可以通过 ref 得到组件的实例,使用后可以直接调用组件的方法或访问数据,这样也可以解决通信问题。
跨多层次组件通信
对于这种情况可以使用 Vue 2.2 新增的 API provide / inject,详细内容后面再做介绍。
任意组件
这种方式可以通过 Vuex 或者 Event Bus 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况。
provide / inject
ref 和 $parent / $children 在跨级通信时是有弊端的。当组件 A 和组件 B 中间隔了数代(甚至不确定具体级别)时,以往会借助 Vuex 或 Bus 这样的解决方案,这里介绍一种无依赖的组件通信方法:Vue.js 内置的 provide / inject 接口。举例如下:
// A.vue
export default {
provide: {
name: 'Aresn'
}
}
// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // Aresn
}
}
tips
一旦注入了某个数据,比如上面示例中的 name,那这个组件中就不能再声明 name 这个数据了,因为它已经被父级占有。
注意:
provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
替代 Vuex
Vuex 是一个专为 Vue.js 开发的状态管理模式,用于集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。使用 Vuex,最主要的目的是跨组件通信、全局数据维护、多人协同开发。需求比如有:用户的登录信息维护、通知信息维护等全局的状态和数据。
我们可以使用 provide / inject 替代 Vuex 在某些场景上的使用。
要达到这个目的,需要在根组件(app.vue)上做文章,比如:把整个 app.vue 实例通过 provide 对外提供。
// app.vue
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
provide () {
return {
app: this
}
}
}
</script>
上面,我们把整个 app.vue 的实例 this 对外提供,命名为 app(这个名字可以自定义,推荐使用 app,使用这个名字后,子组件不能再使用它作为局部属性)。接下来,任何组件(或路由)只要通过 inject 注入 app.vue 的 app 的话,都可以直接通过 this.app.xxx 来访问 app.vue 的 data、computed、methods 等内容。
tips
app.vue 是整个项目第一个被渲染的组件,而且只会渲染一次(即使切换路由,app.vue 也不会被再次渲染),利用这个特性,很适合做一次性全局的状态数据管理
进阶应用
如果你的项目足够复杂,或需要多人协同开发时,在 app.vue 里会写非常多的代码,多到结构复杂难以维护。这时可以使用 Vue.js 的混合 mixins,将不同的逻辑分开到不同的 js 文件里。比如:
// user.js
export default {
data () {
return {
userInfo: null
}
},
methods: {
getUserInfo () {
// 这里通过 ajax 获取用户信息后,赋值给 this.userInfo,以下为伪代码
$.ajax('/user/info', (data) => {
this.userInfo = data;
});
}
},
mounted () {
this.getUserInfo();
}
}
然后在 app.vue 中混合:
<script>
import mixins_user from '../mixins/user.js';
export default {
mixins: [mixins_user],
data () {
return {
//
}
}
}
</script>
dispatch 和 broadcast
在 Vue.js 1.x 中,提供了两个方法:$dispatch 和 $broadcast ,前者用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在组件内通过 on(或 events,2.x 已废弃)监听到,后者相反,是由上级向下级广播事件的。broadcast 类似,只不过方向相反。
这两种方法一旦发出事件后,任何组件都是可以接收到的,就近原则,而且会在第一次接收到后停止冒泡,除非返回 true。
这两个方法虽然看起来很好用,但是在 Vue.js 2.x 中都废弃了,我们可以自行实现:
自行实现 dispatch 和 broadcast 方法
通过目前已知的信息,我们要实现的 dispatch 和 broadcast 方法,将具有以下功能:
在子组件调用 dispatch 方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先通过 $on 监听了这个事件;
相反,在父组件调用 broadcast 方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先通过 $on 监听了这个事件。
该方法可能在很多组件中都会使用,复用起见,我们封装在混合(mixins)里。那它的使用样例可能是这样的:
import Emitter from '../mixins/emitter.js'
export default {
mixins: [ Emitter ],
methods: {
handleDispatch () {
this.dispatch(); // ①
},
handleBroadcast () {
this.broadcast(); // ②
}
}
}
具体实现如下:
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
具体使用方法如下:
<!-- A.vue -->
<template>
<button @click="handleClick">触发事件</button>
</template>
<script>
import Emitter from '../mixins/emitter.js';
export default {
name: 'componentA',
mixins: [ Emitter ],
methods: {
handleClick () {
this.broadcast('componentB', 'on-message', 'Hello Vue.js');
}
}
}
</script>
// B.vue
export default {
name: 'componentB',
created () {
this.$on('on-message', this.showMessage);
},
methods: {
showMessage (text) {
window.alert(text);
}
}
}
相比 Vue.js 1.x,有以下不同:
需要额外传入组件的 name 作为第一个参数;
无冒泡机制;
第三个参数传递的数据,只能是一个(较多时可以传入一个对象),而 Vue.js 1.x 可以传入多个参数,当然,你对 emitter.js 稍作修改,也能支持传入多个参数,只是一般场景传入一个对象足以。