前言
Vue
中不管是每个页面,还是从多个页面中抽取出来的公共代码封装,都属于组件。在Vue
项目中组件开发是必不可少的。那么组件与组件之间必定不是完全分离的,大多业务组件之间涉及到数据交互,组件通信必不可少。那么在Vue
项目中都有哪些通信方式,应用场景分别是怎么样的呢?
为什么要用到组件通信
组件是Vue
最强大的功能之一,组件实例的作用域是相互独立的,这意味着不同组件之间的数据无法相互引用。所有组件都无法直接访问其他组件的data
、methods
、computed
,每个组件都是一个局部作用域,这样做的目的是为了保护私有变量,保证每个组件之间的data
不会相互影响。但是我们需要将组件相互连接起来,这些组件相互之间会有数据依赖以及需要修改从其他组件传递过来的数据,这种情况我们统称为数据通信。
一般来说在Vue
中有组件间的关系分为父子组件、兄弟组件、或者是隔代关系。
不同的通信方式适用于不同的组件关系进行数据通信。
实现组件通信的方式
父子组件传值
1. prop/$emit
prop
一般父组件向子组件传值都使用prop
属性。使用方法也很简单,我们一般将需要传递到子组件的属性通过动态绑定的方式绑定在子组件上,然后在子组件中通过props
来接收。
// 父组件
<template>
<div class="home">
<div>father: {{count}}</div>
<button @click="addCount">father: +1</button>
<hello-world :fatherCount="count" />
</div>
</template>
<script>
import HelloWorld from "../components/HelloWorld.vue";
export default {
name: "Home",
data() {
return {
count: 3
}
},
components: {
HelloWorld
},
methods: {
addCount() {
this.count++
}
}
}
</script>
// 子组件
<template>
<div>
<div class="hello">child: {{fatherCount}}</div>
<button @click="addFatherCount">child: +1</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
fatherCount: {
type: Number,
default: 0
}
},
data() {
return {}
},
methods: {
addFatherCount() {
this.fatherCount++
}
}
};
</script>
我们可以看到,通过动态绑定v-on
在父组件上将数据绑定在子组件上。子组件中通过props
接收父组件传过来的数据。父组件中修改对应数据,子组件中会跟着修嘎,子组件中直接修改从父组件中传过来的数据会报错,但是子组件的页面渲染还是会跟着修改。父组件在对该数据进行修改,即使子组件中数据已被子组件自己修改过,仍然会再次与父组件数据保持一致。那么如果我们想通过子组件修改父组件的数据该怎么处理呢?说实话我们应该尽量避免这种情况的发生,毕竟vue
设置单向数据流机制也是为了避免子组件随意修改父组件的值。
$emit
官方给的介绍是:触发当前实例上的事件,附加参数都会传给监听器回调。所以我们可以在父组件中,将修改数据的函数传给子组件,在子组件中通过$emit
来调用父组件的函数。所以$emit
的本质是子组件调用父组件函数进行数据修改。
那么我们该怎么使用$emit
来从子组件修改父组件的数据呢,我们在上一个栗子上进行修改:
// 修改子组件函数
methods: {
addFatherCount() {
this.$emit('addCount')
}
}
运行看效果,点击子组件+1按钮,也可以正常修改父组件传过来的数据了。
2. $children/$parent
parent
官方介绍:指定已创建的实例之父实例。子实例可以用this.$parent
访问父实例,子实例被推入父实例的$children
数组中。
但是,请注意但是,官方也说了:节制地使用$parent
和$children
,它们的主要目的是作为访问组件的应急方法。更推荐用parent
和events
实现父子组件通信。所以大家在用的时候斟酌一下。
子组件可以通过this.$parent
访问其父组件的实例,可以访问父组件的实例就可以访问该组件的所有方法和data
。我们来看一下该怎么使用$parent
向子组件传递数据,也就是在子组件获得父组件的数据。
// 父组件
<template>
<div class="home">
<div>father: {{count}}</div>
<button @click="addCount">father: +1</button>
<hello-world />
</div>
</template>
<script>
import HelloWorld from "../components/HelloWorld.vue";
export default {
name: "Home",
data() {
return {
count: 3
}
},
components: {
HelloWorld
},
methods: {
addCount() {
this.count++
console.log(this.$children[0].fatherCount)
// 这里如果执行,程序会进入死循环
// this.$children[0].addFatherCount()
}
}
}
</script>
// 子组件
<template>
<div>
<div class="hello">child: {{fatherCount}}</div>
<button @click="addFatherCount">child: +1</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {}
},
computed: {
fatherCount() {
return this.$parent.count
}
},
methods: {
addFatherCount() {
this.$parent.count++
this.$parent.addCount()
}
}
};
</script>
我们可以看到,在子组件中通过this.$parent
可以直接访问父组件的数据,调用父组件函数,并可直接修改父组件数据。在父组件中我们也可以用this.$children[i]
获得对应子组件的数据,也可以直接调用子组件函数。在栗子中,我们注释掉的那一块代码,如果运行起来就会进入死循环,因为在父组件中调用子组件函数,子组件的该函数中调用父组件,循环调用,无限循环。
3. .sync
看官方介绍:在有些情况下,我们可能需要对一个prop
进行"双向绑定"。但是真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件。所以推荐以update.myPropName
的模式触发事件取而代之。
我们看一看.sync
修饰符具体使用方式:
// 父组件
<template>
<div class="home">
<div>father: {{count}}</div>
<button @click="addCount">father: +1</button>
// 将count绑定在子组件上
<hello-world :fatherCount.sync="count" />
</div>
</template>
<script>
import HelloWorld from "../components/HelloWorld.vue";
export default {
name: "Home",
data() {
return {
count: 3
}
},
components: {
HelloWorld
},
methods: {
addCount() {
this.count++
console.log(this.$children[0].fatherCount)
}
}
}
</script>
// 子组件
<template>
<div>
<div class="hello">child: {{fatherCount}}</div>
<button @click="addFatherCount">child: +1</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
fatherCount: {
type: Number,
default: 0
}
},
data() {
return {}
},
methods: {
addFatherCount() {
// 通过该方式修改子组件的值,父组件值跟着改变
this.$emit("update:fatherCount", 5)
}
}
};
</script>
4. ref
ref
被用来给元素或子组件注册引用信息。引用信息会注册到父组件的$refs
对象上。如果在普通地DOM
元素上使用,引用就指向该DOM
元素;如果用在子组件上,引用就指向组件实例。当v-for
用于元素或组件的时候,引用信息将包含DOM
节点或组件实例的数组。所以在父组件中就可以通过ref
主动获取子组件的属性或调用子组件的方法。
注意:关于ref
注册时间的重要说明:因为ref
本身是作为渲染结果被创建的,在初始渲染的时候我们还不能访问它们,因为它们此时并不存在。$refs
也不是响应式的,所以我们不能试图用它在模版中做数据绑定。
// 父组件
<template>
<div class="home">
<button @click="addCount">father: +1</button>
<hello-world ref="child" />
</div>
</template>
<script>
import HelloWorld from "../components/HelloWorld.vue";
export default {
name: "Home",
data() {
return {
count: 0
}
},
components: {
HelloWorld
},
methods: {
addCount() {
const child = this.$refs.child
child.addChildCount();
console.log(child.childCount);
}
}
}
</script>
// 子组件
<template>
<div>
<div class="hello">child: {{childCount}}</div>
<button @click="addChildCount">child: +1</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
childCount: 2
}
},
methods: {
addChildCount() {
this.childCount++
}
}
};
</script>
其他
1. $emit/$on
这种方法通过一个空的Vue
实例作为中央事件的总线(事件中心),用它来触发事件和监听事件,巧妙而轻量的实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案Vuex
。
我们来看一下具体该怎么使用呢:
// 父组件
<template>
<div class="home">
<child-component />
<hello-world />
</div>
</template>
<script>
import HelloWorld from "../components/HelloWorld.vue"
import ChildComponent from "../components/child.vue"
export default {
name: "Home",
data() {
return {
count: 0
}
},
components: {
HelloWorld,
ChildComponent
}
}
</script>
我们实现一个兄弟传值,其实并不限于兄弟组件之间,重点看两个子组件
// HelloWord, 在该组件中派发事件
<template>
<div>
<div class="hello">{{childName}}Count : {{childCount}}</div>
<button @click="sendChildCount">派发事件</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
childName: "helloWord",
childCount: 2
}
},
methods: {
sendChildCount() {
this.$EventBus.$emit('hello-name', this.childName)
this.$EventBus.$emit('hello-count', ++this.childCount)
}
}
};
</script>
// ChildComponent, 在该组件中监听事件的派发
<template>
<div>
<div class="hello">childCount: {{count}}</div>
<div>{{brotherName}}</div>
<div>{{count}}</div>
</div>
</template>
<script>
export default {
name: "ChildComponent",
data() {
return {
childName: "child",
brotherName: "",
count: 0
}
},
created() {
this.$EventBus.$on('hello-name', name => {
this.brotherName = name
console.log(111);
})
this.$EventBus.$on('hello-count', count => {
this.count = count
})
console.log(this);
}
};
一定要注意,这里用的是一个全局事件总线,在于$EventBus
,这是一个空的Vue
实例,为了方便我们把这个事件总线定main.js
中注册在Vue
的原型链上了,所以我们通过this可以访问到该事件总线,不需要我们每次使用都去new
一个空Vue
。
// main.js
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
Vue.prototype.$EventBus = new Vue()
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
2. $attrs、$listeners
$attrs
那么先来大概说一下,什么是$attrs
。
$attrs
包含了父作用域中不作为prop
被识别的attribute
绑定(class
和style
除外)。当一个组件没有声明任何prop
时,这里会包含所有父级作用域的绑定,摒弃可以通过v-bind = '$attrs'
传入内部组件——在创建高级别的组件时非常有用。
$listeners
$listeners
包含了父作用域中(不含.native修饰器的)v-on
事件监听器。可以通过v-on = "$listeners"
传入内部组件。
使用方式
多组件嵌套一般我们会使用vuex
,但是如果仅仅是传递数据,并不做中间处理,就不需要vuex
,我们就可以通过$attrs
与$listeners
来进行数据处理。
接下来看实例:
// 父组件
<template>
<div class="home">
<hello-world :parentCount="count"
:parentName="name"
:parentAge="age"
@addCount="addCount"
@addAge="addAge" />
</div>
</template>
<script>
import HelloWorld from "../components/HelloWorld.vue"
export default {
name: "Home",
data() {
return {
count: 3,
name: 'home',
age: 0
}
},
components: {
HelloWorld
},
methods: {
addCount() {
this.count++
},
addAge() {
this.age++
}
}
}
</script>
// 子组件
<template>
<div>
<div class="hello">parentCount: {{parentCount}}</div>
<div>attrs: {{$attrs}}</div>
<button @click="addParentCount">count+1</button>
<child-component v-bind="$attrs"
v-on="$listeners"
@addParentCount="addParentCount" />
</div>
</template>
<script>
import ChildComponent from './child.vue'
export default {
name: "HelloWorld",
components: {
ChildComponent
},
props: {
parentCount: {
type: Number,
default: 0
}
},
methods: {
addParentCount() {
console.log(this.$listeners) // 包含父级所有绑定的方法
this.$listeners.addCount()
}
}
};
</script>
// 子组件的子组件
<template>
<div>
<hr />
<div class="hello">grandpaName: {{parentName}}</div>
<div>attrs: {{$attrs}}</div>
<button @click="addCount">age+1</button>
</div>
</template>
<script>
export default {
name: "ChildComponent",
props: {
parentName: {
type: String,
default: ''
}
},
methods: {
addCount() {
console.log(this.$listeners) // 包含父级所有绑定的方法
this.$listeners.addAge()
}
}
};
</script>
根据运行结果,我们可以知道$attrs
里存放的是父组件中绑定的非 Props
属性,$listeners
里存放的是父组件中绑定的非原生事件。我们可以将$attrs
与$listeners
传递给子组件,在后代组件中可访问对应的属性与方法。
provide、inject
官方介绍:这对选项需要一起使用,以允许一个祖先组件向其所有后代组件注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
provide
选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的property
。在该对象中你可以使用symbol
作为key
。
indect
选项应该是:
- 一个字符串数组或者
- 一个对象,对象的
key
是本地的绑定名,value
是- 在可用的注入内容中搜索用的
key
(字符串或Symbol
),或 - 一个对象,该对象的:
from property
是在可用的注入内容中搜索用的key
(字符串或Symbol
)default property
是降级情况下使用的value
- 在可用的注入内容中搜索用的
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
使用方法
我们来个实例看一下具体使用方法:
// 祖先组件
<template>
<div class="home">
count: {{count}}
<hello-world />
</div>
</template>
<script>
import HelloWorld from "../components/HelloWorld.vue"
import Vue from "vue"
export default {
name: "Home",
data() {
return {
count: 3,
name: 'home',
age: 0
}
},
components: {
HelloWorld
},
provide() {
return {
parentCount: this.count,
addCount: this.addCount
}
},
methods: {
addCount() {
this.count++
}
}
}
</script>
// 后代组件
<template>
<div>
<hr />
<button @click="addCount">count+1</button>
<div class="hello">parentCount: {{parentCount}}</div>
</div>
</template>
<script>
export default {
name: "ChildComponent",
inject: {
parentCount: {
type: Number,
default: 0
},
addCount: {
type: Function,
default: () => { }
}
}
};
</script>
在后代组件中都可以获得祖先组件通provide
传递的属性和函数。
Vuex
最后在过于复杂的组件传值并且可能需要在中间做一些数据处理,我们就可以用Vuex去处理,关于Vuex我们就不过多讲解。