vue 组件间的通信是 vue 开发中很基础也十分重要的部分,作为使用 vue 的开发者每天都在使用。同时,vue 通信也是面试中非常高频的问题,有很多面试题,都是围绕通信展开。
本文会介绍常见的通信方式,并分析每种方式的使用场景和注意点。
vue中提倡单向数据流,这是为了保证数据流向的简洁性,使程序更易于理解。但对于一些边界情况,vue也提供了隐性的通信方式,这些通信方式会打破单向数据流的原则,应该谨慎使用。
下面我们将组件通信分为父子组件通信 和 非父子组件通信进行分析
父子组件通信
1,prop 和 events
prop
和 events
最基础也最常用,这里不提供示例。
通过 prop
向下传递,通过事件向上传递是一个 vue 项目最理想的通信状态。
<!--父组件-->
<template>
<base-input v-model="input"></base-input>
</template>
<script>
export default {
data() {
return {
input: ''
}
},
}
</script>
<!--子组件-->
<template>
<input type="text" :value="currentValue" @input="handleInput">
</template>
<script>
export default {
data() {
return {
currentValue: this.value === undefined || this.value === null ? ''
}
},
props: {
value: [String, Number],
},
methods: {
handleInput(event) {
const value = event.target.value;
this.$emit('input', value);
},
},
}
</script>
2,ref
ref 特性可以为子组件赋予一个 ID 引用,通过这个 ID 引用可以直接访问这个子组件的实例。当父组件中需要主动获取子组件中的数据或者方法时,可以使用 $ref
来获取。
<!--父组件-->
<template>
<base-input ref="baseInput"></base-input>
</template>
<script>
export default {
methods: {
focusInput: function () {
this.$refs.usernameInput.focus()
}
}
}
</script>
<!--子组件-->
<template>
<input ref="input">
</template>
<script>
export default {
methods: {
focus: function () {
this.$refs.input.focus()
}
}
}
</script>
使用 ref 时,有两点需要注意
$refs
是作为渲染结果被创建的,所以在初始渲染的时候它还不存在,此时无法无法访问。
$refs
不是响应式的,只能拿到获取它的那一刻子组件实例的状态,所以要避免在模板和计算属性中使用它。
3,$parent 和 $children
$parent
属性可以用来从一个子组件访问父组件的实例,$children
属性 可以获取当前实例的直接子组件。
看起来使用 $parent
比使用prop传值更加简单灵活,可以随时获取父组件的数据或方法,又不像使用 prop
那样需要提前定义好。但使用 $parent
会导致父组件数据变更后,很难去定位这个变更是从哪里发起的,所以在绝大多数情况下,不推荐使用。
在有些场景下,两个组件之间可能是父子关系,也可能是更多层嵌套的祖孙关系,这时就可以使用 $parent
。
下面是 element ui 中的组件 el-radio-group 和 组件 el-radio 使用示例:
<template>
<el-radio-group v-model="radio1">
<el-radio :label="3">备选项</el-radio>
<component-1>
<el-radio :label="3">备选项</el-radio>
</component-1>
</el-radio-group>
</template>
<script>
export default {
data () {
return {
radio2: 3
};
}
}
</script>
在 el-radio-group 和 组件 el-radio 通信中, 组件 el-radio 的 value 值需要和 el-radio-group的 v-model
的值进行“绑定”,我们就可以在 el-radio 内借助 $parent
来访问到 el-radio-group 的实例,来获取到 el-radio-group 中 v-model
绑定的值。
下面是获取 el-radio 组件中获取 el-radio-group 实例的源码:
// el-radio组件
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent;
} else {
this._radioGroup = parent; // this._radioGroup 为组件 el-radio-group 的实例
}
}
非父子组件通信
4,$attrs 和 $listeners
$attrs
会包含父组件中没有被 prop
接收的所有属性(不包含class 和 style 属性),可以通过 v-bind="$attrs"
直接将这些属性传入内部组件。
$listeners
会包含所有父组件中的 v-on
事件监听器 (不包含 .native
修饰器的) ,可以通过 v-on="$listeners"
传入内部组件。
下面以父组件和孙子组件的通信为例介绍它们的使用:
<!--父组件 parent.vue-->
<template>
<child :name="name" :message="message" @sayHello="sayHello"></child>
</template>
<script>
export default {
inheritAttrs: false,
data() {
return {
name: '通信',
message: 'Hi',
}
},
methods: {
sayHello(mes) {
console.log('mes', mes) // => "hello"
},
},
}
</script>
<!--子组件 child.vue-->
<template>
<grandchild v-bind="$attrs" v-on="$listeners"></grandchild>
</template>
<script>
export default {
data() {
return {}
},
props: {
name,
},
}
</script>
<!--孙子组件 grand-child.vue-->
<template>
</template>
<script>
export default {
created() {
this.$emit('sayHello', 'hello')
},
}
</script>
5,provide 和 inject
provide
和 inject
需要在一起使用,它可以使一个祖先组件向其所有子孙后代注入一个依赖,可以指定想要提供给后代组件的数据/方法,不论组件层次有多深,都能够使用。
<!--祖先组件-->
<script>
export default {
provide: {
author: 'yushihu',
},
data() {},
}
</script>
<!--子孙组件-->
<script>
export default {
inject: ['author'],
created() {
console.log('author', this.author) // => yushihu
},
}
</script>
provide
和 inject
绑定不是响应的,它被设计是为组件库和高阶组件服务的,平常业务中的代码不建议使用。
6,dispatch 和 broadcast
vue 在2.0版本就已经移除了 $dispatch
和 $broadcast
,因为这种基于组件树结构的事件流方式会在组件结构扩展的过程中会变得越来越难维护。但在某些不使用 vuex 的情况下,仍然有使用它们的场景。所以 element ui 和 iview 等开源组件库中对 broadcast
和 dispatch
方法进行了重写,并通过 mixin 的方式植入到每个组件中。
实现 dispatch
和 broadcast
主要利用我们上面已经说过的 $parent
和 $children
//element ui 中重写 broadcast 的源码
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
broadcast
方法的作用是向后代组件传值,它会遍历所有的后代组件,如果后代组件的 componentName
与当前的组件名一致,则触发 $emit
事件,将数据 params
传给它。
//element ui 中重写 dispatch 的源码
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
}
dispatch
的作用是向祖先组件传值,它会一直寻找父组件,直到找到组件名和当前传入的组件名一致的祖先组件,就会触发其身上的 $emit
事件,将数据传给它。这个寻找对应的父组件的过程和文章前面讲解 $parent
的例子类似。
7,eventBus
对于比较小型的项目,没有必要引入 vuex 的情况下,可以使用 eventBus
。相比我们上面说的所有通信方式,eventBus
可以实现任意两个组件间的通信。
它的实现思想也很好理解,在要相互通信的两个组件中,都引入同一个新的vue实例,然后在两个组件中通过分别调用这个实例的事件触发和监听来实现通信。
//eventBus.js
import Vue from 'vue';
export default new Vue();
<!--组件A-->
<script>
import Bus from 'eventBus.js';
export default {
methods: {
sayHello() {
Bus.$emit('sayHello', 'hello');
}
}
}
</script>
<!--组件B-->
<script>
import Bus from 'eventBus.js';
export default {
created() {
Bus.$on('sayHello', target => {
console.log(target); // => 'hello'
});
}
}
</script>
8,通过 $root 访问根实例
通过 $root
,任何组件都可以获取当前组件树的根 Vue 实例,通过维护根实例上的 data
,就可以实现组件间的数据共享。
//main.js 根实例
new Vue({
el: '#app',
store,
router,
// 根实例的 data 属性,维护通用的数据
data: function () {
return {
author: ''
}
},
components: { App },
template: '<App/>',
});
<!--组件A-->
<script>
export default {
created() {
this.$root.author = '于是乎'
}
}
</script>
<!--组件B-->
<template>
<div><span>本文作者</span>{{ $root.author }}</div>
</template>
通过这种方式,虽然可以实现通信,但在应用的任何部分,任何时间发生的任何数据变化,都不会留下变更的记录,这对于稍复杂的应用来说,调试是致命的,不建议在实际应用中使用。
9,Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。对一个中大型单页应用来说是不二之选。
使用 Vuex 并不代表就要把所有的状态放入 Vuex 管理,这样做会让代码变的冗长,无法直观的看出要做什么。对于严格属于组件私有的状态还是应该在组件内部管理更好。
10,自己实现简单的 Store 模式
对于小型的项目,通信十分简单,这时使用 Vuex 反而会显得冗余和繁琐,这种情况最好不要使用 Vuex,可以自己在项目中实现简单的 Store。
//store.js
var store = {
debug: true,
state: {
author: 'yushihu!'
},
setAuthorAction (newValue) {
if (this.debug) console.log('setAuthorAction triggered with', newValue)
this.state.author = newValue
},
deleteAuthorAction () {
if (this.debug) console.log('deleteAuthorAction triggered')
this.state.author = ''
}
}
和 Vuex 一样,store 中 state
的改变都由 store 内部的 action
来触发,并且能够通过 log
保留触发的痕迹。这种方式十分适合在不需要使用 Vuex 的小项目中应用。
与 $root
访问根实例的方法相比,这种集中式状态管理的方式能够在调试过程中,通过 log
记录来确定当前变化是如何触发的,更容易定位问题。