Vue3组件通讯
vue3组件之间并不是孤立的,它们之间是需要通信的,正是这种组件间的相互通信才构成了页面上用户行为交互的过程。
一. 父组件向子组件通信
父组件向子组件通信可以理解成:
- 父组件向子组件传值。
- 父组件调用子组件的方法。
-
props
利用props属性可以实现父组件向子组件传值,eg :
const componentC={
props:['info'],//子组件使用props接收
data(){
return {
str:'I am C'
}
},
template: `<span>{{ str }} : {{info}} </span>`
}
//定义名为compontentB的父组件,
const componentB={
data(){
return {
str :'I am B',
info:'data from B'
}
},
template: `
<div>
{{str}}
<div>
<component-c :info='info' />
</div>
</div>
`,
components : {
'component-c': componentC,
}
}
props不仅可以传字符串类型的值,像数组、对象、布尔值都可以传递,在传递时props也可以定义成数据校验的形式,以此来限制接收的数据类型,提升规范性,避免得到意想之外的值,type可以是下列原生构造函数中的一个: String、Number、Boolean、Array、Object、Date、Function、Symbol。type还可以是一个自定义的构造函数,并且通过instanceof来进行检查确认
如果props传递的是一个动态值,每次父组件的info发生更新时,子组件中接收的props都将会刷新为最新的值。这意味着,我们不应该在一个子组件内部改变props,如果这样做了,Vue会在浏览器的控制台中发出警告。
Vue中父传子的方式形成了一个单向下行绑定,叫作单向数据流。父级props的更新会向下流动到接收这个props的子组件中,触发对应的更新,但是反过来则不行。这样可以防止有多个子组件的父组件内的值被修改时,无法查找到哪个子组件修改的场景,从而导致应用中的数据流向无法清晰地追溯。
-
$refs
利用Vue实例的$refs属性可以实现父组件调用子组件的方法,eg:
const componentC={
props:['info'],//子组件使用props接收
data(){
return {
str:'I am C'
}
},
template: `<span>{{ str }} : {{info}} </span>`,
methods:{
doFunc(){
console.info('do func C')
}
}
}
//定义名为compontentB的父组件,
const componentB={
data(){
return {
str :'I am B',
info:'data from B'
}
},
template: `
<div>
{{str}}
<div>
<component-c :info='info' ref='componentC'/>
</div>
</div>
`,
components : {
'component-c': componentC,
},
mounted(){
this.$refs.componentC.doFunc()
}
}
在Vue中,也可以给原生的DOM元素绑定ref值,这样通过this.$refs拿到的就是原生的DOM对象
<button ref="btn"></button>
二. 子组件向父组件通信
与父组件向子组件通信不同的是,子组件调用父组件方法的同时就可以向父组件传值,使用$emit方法和自定义事件。
1.$emit
$emit 方法的主要作用是触发当前组件实例上的事件,所以子组件调用父组件方法就可以理解成子组件触发了绑定在父组件上的自定义事件。
//子组件
mounted():{
this.$emit('myFunction','hi')
}
//父组件
{
template: '<component-c @myFunction='myFunction' />',
methods: {
//定义父组件需要被子组件调用的方法
myFunction(data){
console.log('来自子组件的调用',data)
}
}
}
2.$parent
这种方法比较直观,可以直接操作父子组件的实例,在子组件中直接通过this.$parent
获取父组件的实例,从而调用父组件中定义的方法,类似于以上介绍的通过$refs获取子组件的实例。
//子组件
mounted() {
//直接采用$parent调用
this.$parent.myFunction('$parent方法调用')
}
//父组件
methods:{
myFunction(data){
console.log('来自子组件的调用',data)
}
}
Vue并不推荐以这种方法来实现子组件调用父组件,由于一个父组件可能会有多个子组件,因此这种方法对父组件中的状态维护是非常不利的,当父组件的某个属性被改变时,无法以循规溯源的方式去查找到底是哪个子组件改变了这个属性。因此,请有节制地使用$parent
方法,它的主要目的是作为访问组件的应急方法。推荐使用$emit
方法实现子组件向父组件的通信。
三. 父子组件的双向数据绑定与自定义v-model
父组件可以使用props给子组件传值,当父组件props更新时也会同步给子组件,但是子组件无法直接修改父组件的props,这其实是一个单向的过程。但是在一些情况下,我们可能会需要对一个props进行“双向绑定”,即子组件的值更改后,父组件也同步进行更改。
在父组件的data中定义info属性,并且通过v-model的方式传给了子组件,代码如下:
<component-d v-model:info="info" />
这里使用$emit方法,在子组件中,给按钮button绑定了一个单击事件,在事件回调函数中,采用如下代码:
this.$emit('update:info','Tom')
这样更新就会同步到父组件的props中,调用$emit方法实际上就是触发一个父组件的方法,这里的update是固定写法,代表更新,而:info表示更新info这个prop,第二个参数Tom表示更新的值。这就完成了父子组件的“双向绑定”。
四. 非父子关系组件的通信
- 兄弟组件的通信
同一个父组件B的兄弟组件C和D而言,可以借助父组件B这个桥梁,实现兄弟组件的通信
在D组件中通过$emit调用父组件B的方法,同时在父组件中修改data中的infoFromD(infoFromD属性绑定到了组件C的infoFromD),同时也影响到了作为props传递给C组件的infoFromD,这就实现了兄弟组件的通信。 - 事件总线EventBus和mitt
在Vue 2中,可以采用EventBus这种方法,实际上就是将沟通的桥梁换成自己,同样需要有桥梁作为通信中继。就像是所有组件共用相同的事件中心,可以向该中心发送事件或接收事件,所有组件都可以上下平行地通知其他组件。
vue2中的实现
//定义全局中央事件总线
var EventBus = new Vue();
//将事件总线赋值给Vue.prototype 这样所有组件就都能访问到了
Vue.prototype.$EventBus = EventBus;
//监听事件
this.$EventBus.$on('eventBusEvent',(data)=>{
//处理逻辑
}).bind(this)
//触发事件
this.$EventBus.$emit('eventBusEvent','我的事件总线')
通过new Vue()实例化了一个Vue的实例,这个实例是一个没有任何方法和属性的空实例,称其为:中央事件总线(EventBus),然后将其赋值给Vue.prototype.$EventBus
,使得所有的组件都能够访问到。
$on
方法和$emit
方法其实都是Vue实例提供的方法,这里的关键点就是利用一个空的Vue实例来作为桥梁,实现事件分发,它的工作原理是发布/订阅方法,通常称为Pub/Sub,也就是发布和订阅的模式。
在Vue 3中,由于取消了Vue中全局变量Vue.prototype.$EventBus这种写法,所以采用EventBus这种事件总线来进行通信已经无法使用,取而代之,可以采用第三方事件总线库mitt。
在页面中引入mitt的JavaScript文件或者在项目中采用import方式引入mitt,
<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
//or
import mitt from 'mitt'
mitt的使用方法和EventBus非常类似,同样是基于Pub/Sub模式,并且更加简单,可以在需要进行通信的地方直接使用
const emitter = mitt();
//监听
emitter.on('eventBusEvent', (data) => {
// todo
})
//触发
clickCallback() {
emitter.emit('eventBusEvent', '我是...')
}
与EventBus相比,mitt的方式无须创建全局变量,使用起来更加简单。
事件总线的方式进行通信使用起来非常简单,可以实现任意组件之间的通信,其中没有多余的业务逻辑,只需要在状态变化组件触发一个事件,随后在处理逻辑组件监听该事件即可。这种方法非常适合小型的项目,但是对于一些大型的项目,要实现复杂的组件通信和状态管理,就需要使用Vuex了。
五. provide / inject
通常,当需要从父组件向子组件传递数据时,我们使用props。想象一下这样的结构:有一些深度嵌套的组件,深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将props沿着组件链逐级传递下去,可能会很麻烦。
对于这种情况,我们可以使用一对provide(提供)和inject(注入)。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个provide选项来提供数据,子组件有一个inject选项来开始使用这些数据
//父组件
{
provide: {
user: ' john'
}
}
//子组件
{
inject: ['user'],
created(){
this.user;
}
}
要访问组件实例data中的属性,我们需要将provide转换为返回对象的函数。