vue 组件间的通讯是 vue 开发中很基础也十分重要的部分,做为使用 vue 的开发者天天都在使用。 同时,vue 通讯也是面试中很是高频的问题,有不少面试题,都是围绕通讯展开。javascript
本文会介绍常见的通讯方式,并分析每种方式的使用场景和注意点。html
vue中提倡单向数据流,这是为了保证数据流向的简洁性,使程序更易于理解。但对于一些边界状况,vue也提供了隐性的通讯方式,这些通讯方式会打破单向数据流的原则,应该谨慎使用。前端
下面咱们将组件通讯分为父子组件通讯 和 非父子组件通讯进行分析。vue
prop
和 events
最基础也最经常使用,这里不提供示例。
经过 prop
向下传递,经过事件向上传递是一个 vue 项目最理想的通讯状态。
使用时有两点须要注意:java
第一,不该该在一个子组件内部改变
prop
,这样会破坏单向的数据绑定,致使数据流难以理解。若是有这样的须要,能够经过data
属性接收或使用computed
属性进行转换。
第二,若是props
传递的是引用类型(对象或者数组),在子组件中改变这个对象或数组,父组件的状态会也会作相应的更新,利用这一点就可以实现父子组件数据的“双向绑定”,虽然这样实现可以节省代码,但会牺牲数据流向的简洁性,使人难以理解,最好不要这样去作。想要实现父子组件的数据“双向绑定”,可使用v-model
或.sync
。面试
v-model
是用来在表单控件或者组件上建立双向绑定的,他的本质是 v-bind
和 v-on
的语法糖,在一个组件上使用 v-model
,默认会为组件绑定名为 value
的 prop
和名为 input
的事件。
当咱们组件中的某一个 prop
须要实现上面所说的”双向绑定“时,v-model
就能大显身手了。有了它,就不须要本身手动在组件上绑定监听当前实例上的自定义事件,会使代码更简洁。vuex
下面以一个 input 组件实现的核心代码,介绍下 v-model
的应用。数组
<!--父组件-->
<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>
复制代码
有时,在某些特定的控件中名为 value
的属性会有特殊的含义,这时能够经过 model
选项来回避这种冲突。app
.sync
修饰符在 vue 1.x 的版本中就已经提供,1.x 版本中,当子组件改变了一个带有 .sync
的 prop
的值时,会将这个值同步到父组件中的值。这样使用起来十分方便,但问题也十分明显,这样破坏了单向数据流,当应用复杂时,debug 的成本会很是高。因而乎,在vue 2.0中移除了 .sync
。 可是在实际的应用中,.sync
是有它的应用场景的,因此在 vue 2.3 版本中,又迎来了全新的 .sync
。iview
新的 .sync
修饰符所实现的已经再也不是真正的双向绑定,它的本质和 v-model
相似,只是一种缩写。
<text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" ></text-document>
复制代码
上面的代码,使用 .sync
就能够写成
<text-document v-bind:title.sync="doc.title"></text-document>
复制代码
这样,在子组件中,就能够经过下面代码来实现对这个 prop
从新赋值的意图了。
this.$emit('update:title', newTitle)
复制代码
v-model 和 .sync 对比
.sync
从功能上看和v-model
十分类似,都是为了实现数据的“双向绑定”,本质上,也都不是真正的双向绑定,而是语法糖。
相比较之下,.sync
更加灵活,它能够给多个prop
使用,而v-model
在一个组件中只能有一个。
从语义上来看,v-model
绑定的值是指这个组件的绑定值,好比 input 组件,select 组件,日期时间选择组件,颜色选择器组件,这些组件所绑定的值使用v-model
比较合适。其余状况,没有这种语义,我的认为使用.sync
更好。
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
不是响应式的,只能拿到获取它的那一刻子组件实例的状态,因此要避免在模板和计算属性中使用它。
$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 的实例
}
}
复制代码
当要和一个嵌套很深的组件进行通讯时,若是使用 prop
和 events
就会显的十分繁琐,中间的组件只起到了一个中转站的做用,像下面这样:
<!--父组件-->
<parent-component :message="message">我是父组件</parent-component>
<!--子组件-->
<child-component :message="message">我是子组件</child-component>
<!--孙子组件-->
<grand-child-component :message="message">我是孙子组件</grand-child-component>
复制代码
当要传递的数据不少时,就须要在中间的每一个组件都重复写不少遍,反过来从后代组件向祖先组件使用 events 传递也会有一样的问题。使用 $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>
复制代码
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
绑定不是响应的,它被设计是为组件库和高阶组件服务的,日常业务中的代码不建议使用。
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
的例子相似。
对于比较小型的项目,没有必要引入 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>
复制代码
经过 $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>
复制代码
经过这种方式,虽然能够实现通讯,但在应用的任何部分,任什么时候间发生的任何数据变化,都不会留下变动的记录,这对于稍复杂的应用来讲,调试是致命的,不建议在实际应用中使用。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的全部组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。对一个中大型单页应用来讲是不二之选。
使用 Vuex 并不表明就要把全部的状态放入 Vuex 管理,这样作会让代码变的冗长,没法直观的看出要作什么。对于严格属于组件私有的状态仍是应该在组件内部管理更好。
对于小型的项目,通讯十分简单,这时使用 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
记录来肯定当前变化是如何触发的,更容易定位问题。
欢迎关注个人公众号「前端小苑」,我会按期在上面更新原创文章。