背景
Vue是单页面应用,单页面应用又是由组件构成,各个组件之间又互相关联,那么如何实现组件之间通信就显得尤为重要了。就像人是由各种器官组成,那么组件之间的通信就像是血液一样将营养(数据)输送到各个部位,为了保证数据流向的简洁性,使程序更易于理解,所以vue提倡单项数据流。组件之间的通信主要分为三种,父子组件通信、孙子组件通信和非关联组件通信。
父子组件通信
props和$emit
这种方式是大家最经常用到的,父组件通过v-bind绑定数据,子组件通过props接收父组件传过来的数据,利用$emit触发指定事件,父组件通过$on监听子组件触发的对应事件,这里就不举例了。主要讲一下prop:
- Vue数据是单向数据流,这样设计的目的是为了保证数据流向的简洁性,使程序更易于理解,每次父级组件发生更新时,子组件中所有prop都将会刷新为最新的值,prop会在子组件创建之前传递,所以可以在data和computed中直接使用。
- 不应该在一个子组件内部改变prop,这样会破坏单向的数据绑定,导致数据流难以理解。如果有这样的需要,可以通过 data 属性接收或使用 computed 属性进行转换。
- 如果prop传递的是基本类型,这时候改变prop的数据就会报错,如果是引用数据类型如果改变原始数据不会报错,但是重新赋值或改变某个属性值就会报错,利用这一点就能够实现父子组件数据的“双向绑定”,虽然这样实现能够节省代码,但会牺牲数据流向的简洁性,令人难以理解,最好不要这样去做。想要实现父子组件的数据“双向绑定”,可以使用 v-model 或 .sync,下面会讲到。
v-model
v-model实现父子组件数据的双向绑定,它的本质是v-bind和v-on的语法糖,在一个组件上使用v-model,默认会为组件绑定名为value的属性和名为input的事件。例如:
<test-model
v-bind:value="haorooms"
v-on:input="haorooms=$event"></test-model>
<script>
export default {
data () {
haorooms: ''
}
}
</script>
等价于
<test-model v-model="haorooms"></test-model>
<script>
export default {
data () {
haorooms: ''
}
}
</script>
子组件
<template>
<div>
<input
v-bind:value="value"
v-on="$emit('input', $event.target.value)">
</div>
</template>
<script>
export default {
props: ['value'],
model: {
prop: 'value',
event: 'input'
}
}
</script>
.sync
.sync修饰符它的本质和v-model类似,它的本质也是v-bind和v-on的语法糖,例如:
<test-model
v-bind:title="doc.title"
v-on:update:title="doc.title=$event"></test-model>
<script>
export default {
data () {
doc: {
title: ''
}
}
}
</script>
等价于
<test-model v-bind:title.sync="doc.title"></test-model>
<script>
export default {
data () {
doc: {
title: ''
}
}
}
</script>
子组件
<template>
<div>
<input v-model="value">
</div>
</template>
<script>
export default {
data () {
value: ''
},
watch: {
value (val) {
this.$emit('update:title', val)
}
}
}
</script>
这种是绑定一个值的情况,还可以绑定多个值:
<template>
<div id="demo">
<test-model v-bind.sync="haorooms"></test-model>
</div>
</template>
<script>
import testModel from './testModel'
export default {
data(){
return{
haorooms: {
name: 'aaa',
age: 18,
value: 10
}
}
},
components: {
testModel,
},
watch: {
haorooms: {
handler (val) {
console.log('test', val)
},
deep: true
}
}
}
</script>
子组件
<template>
<div></div>
</template>
<script>
export default {
data () {
return {
test: ''
}
},
mounted () {
this.$emit('update:name', 111)
}
}
</script>
我们看到这种方式是将haorooms对象中name、age、value三个属性都实现了双向绑定,在子组件中触发事件的时候需要指定某个属性来触发。
注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model。
将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
v-model和.sync对比
共同点:
- v-model和.sync都可以实现父子组件数据的双向绑定,本质都是v-bind和v-on的语法糖。
不同点: - v-model默认会为组件绑定名为value的属性和名为input的事件,而.sync可以自定义传入的属性和事件。
- v-model只能实现某一个属性的双向绑定,.sync可以实现多个属性的双向绑定。
- 写法不同v-model需要在子组件中写porp来接收数据,而.sync是利用$attrs来接收数据,所以不需要写prop。
$parent、$children和ref
这三种方式都是通过直接得到组件实例,可以实现父子组件、兄弟组件、跨级组件等数据通信,不过一般不建议使用,因为会增加组件间的耦合,而且要判断组件存不存在,如果不存在可能会遇到报错,这几种方式比较简单也不在这里举例了。
跨级组件
$attrs和$listeners
$attrs
背景
随着项目复杂度的提高,组件嵌套的层级越来越深,之前的组件通信一般使用v-bind和prop组合使用,但是我们发现这种方式只是适用于父子组件,如果是孙子组件的话就需要将父组件的数据传递给子组件,子组件的数据再传递给孙组件,这样子就需要写很多prop,有没有哪种方式可以直接将父组件直接传递给孙组件,让代码更加简洁?这就是$attrs的由来,解决跨组件数据传递,注意只对孙子组件有效,但是class和style数据传递除外。
使用
首先我们有三个嵌套组件父A-子B-孙C,然后我们想让A中的数据传入C中,用prop的做法是这样子的:
<div id="app">
A{{msg}}
<component-b :msg="msg"></component-b>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: '100'
},
components: {
'ComponentB': {
props: ['msg'],
template: `<div>B<component-c :msg="msg"></component-c></div>`,
components: {
'ComponentC': {
props: ['msg'],
template: '<div>C{{msg}}</div>'
}
}
},
}
})
</script>
组件B并没有使用父组件传递过来的msg,而是直接传递给组件C,除了这样子有没有什么方式直接将数据传递给C呢,下面我们来看$attrs的写法:
<script>
let vm = new Vue({
el: '#app',
data: {
msg: '100'
},
components: {
'ComponentB': {
template: `<div>B<component-c v-bind="$attrs"></component-c></div>`,
components: {
'ComponentC': {
props: ['msg'],
template: '<div>C{{msg}}</div>'
}
}
},
}
})
</script>
总结:为了解决跨组件通信,而提出了$attrs。prop和$attrs都可以用来父子组件通信,接收父组件传递过来的数据,但是prop的优先级高于$attrs,如果子组件中prop、$attrs都有写,那么数据只会被prop接收,注意$attrs不能接收class和style传过来的数据。
inheritAttrs
背景
<template>
<div class="home">
<mytest :title="title" :message="message"></mytest>
</div>
</template>
<script>
export default {
name: 'home',
data () {
return {
title:'title1111',
message:'message111'
}
},
components:{
'mytest':{
template:`<div>这是个h1标题{{title}}</div>`,
props:['title'],
data(){
return{
meg:'111'
}
},
created:function(){
console.log(this.$attrs)//注意这里
}
}
}
}
</script>
上面的代码,我们在组件里只是用了title这个属性,message属性没有用到,那么下浏览器渲染出来是什么样呢?如下图:
我们看到:组件内未被注册的属性将作为普通html元素属性在子组件的根元素上渲染,虽然在一般情况下不会对子组件造成影响,但是就怕遇到一些特殊情况,比如:
<template>
<childcom :name="name" :age="age" type="text"></childcom>
</template>
<script>
export default {
'name':'test',
props:[],
data(){
return {
'name':'张三',
'age':'30',
'sex':'男'
}
},
components:{
'childcom':{
props:['name','age'],
template:`<input type="number" style="border:1px solid blue">`,
}
}
}
</script>
我们看到父组件的type="text"覆盖了input上type="number",这不是我想要的,我需要input上type=number类型不变,但是我还是想取到父组件上的type="text"的值,这时候inheritAttrs就派上用场了。
<template>
<childcom :name="name" :age="age" type="text"></childcom>
</template>
<script>
export default {
'name':'test',
props:[],
data(){
return {
'name':'张三',
'age':'30',
'sex':'男'
}
},
components:{
'childcom':{
inheritAttrs:false,
props:['name','age'],
template:`<input type="number" style="border:1px solid blue">`,
created () {
console.log(this.$attrs.type)
}
}
}
}
</script>
总结:默认情况下父组件传递数据给子组件但是没被prop特性绑定将会回退且作为普通的html特性应用在子组件的根元素上。inheritAttrs属性用来去掉这种默认行为,来避免不可预知的影响。注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。
$listeners
背景
上面讲了$attrs是为了跨组件传递数据,那如果想通过孙子组件来给父组件传递数据呢?之前的做法也是一层一层的向上传递,比如用$emit方法,但是子组件如果用不到,只是想改变父组件的数据,这时候我们就可以使用$listeners。
<template>
<div>
<childcom :name="name" :age="age" :sex="sex" @testChangeName="changeName"></childcom>
</div>
</template>
<script>
export default {
'name':'test',
props:[],
data(){
return {
'name':'张三',
'age':'30',
'sex':'男'
}
},
components:{
'childcom':{
props:['name'],
template:`<div>
<div>我是子组件 {{name}}</div>
<grandcom v-bind="$attrs" v-on="$listeners"></grandcom>
</div>`,
components: {
'grandcom':{
template:`<div>我是孙子组件-------<button @click="grandChangeName">改变名字</button></div>`,
methods:{
grandChangeName(){
this.$emit('testChangeName','kkkkkk')
}
}
}
}
}
},
methods:{
changeName(val){
this.name = val
}
}
}
</script>
$listeners是一个对象,里面包含了作用在这个组件上的所有监听器。
参考文章:https://www.jianshu.com/p/ce8ca875c337
provide和inject
背景
项目越复杂,组件嵌套的层级就越深,那么子组件怎样实现跟祖先组件的通信问题,这就是provide和inject提出的原因。
简介
这个方法允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件嵌套的层次有多深,并在起上下游关系成立的时间里始终有效。一言以蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。例如:
假设有两个组件:A和B,A是B组件的祖先组件
// A.vue
export default {
provide: {
name: '天涯'
}
}
// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // 天涯
}
}
我们可以看到在祖先组件A中提供了一个变量,那么在其所有的后代组件中都可以注入这个变量并使用。
所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是天涯。
实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:
- 祖先组件不需要知道哪些后代组件使用它提供的属性
- 后代组件不需要知道被注入的属性来自哪里
然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用$root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像Vuex这样真正的状态管理方案了。
非关联组件通信
vuex
vuex想必大家都非常熟悉了,它是vue的状态管理管理中心,存储的数据是响应式的,但并不会保存起来,刷新之后就回到了初始状态,具体做法应该在vuex数据发生改变的时候把数据拷贝一份保存到localStorage里面,这样子也可以实现实现父子组件、兄弟组件、跨级组件、非关联组件等数据通信。在这里也不再举例。
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
通过$root根组件任何组件都可以获取当前组件树的根vue实例,通过维护根实例上的data,就可以实现组件间的数据共享。
// 组件A
<script>
export default {
created() {
this.$root.$emit('changeTitle', '我是A')
}
}
</script>
// 组件B
<script>
export default {
created() {
this.$root.$on('changeTitle')
},
methods: {
changeTitle (title) {
console.log(title)
}
}
}
</script>
这种方式有个弊端就是组件A和组件B必须同时存在。下面用另外一种方式可以优化:
//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>
通过这种方式,虽然可以实现通信,但在应用的任何部分,任何时间发生的任何数据变化,都不会留下变更的记录,这对于稍复杂的应用来说,调试是致命的,不建议在实际应用中使用。
总结:
本文讲了组件之间的各种通信:
父子组件通信:prop和$emit、v-model、.sync、$parent、children和ref
父子组件双向绑定:props和$emit、v-model、.sync
跨级组件通信:$attrs和$listeners、provide和inject
非关联组件通信:vuex、eventBus、$root