共计 2427 个字符,预计需要花费 7 分钟才能阅读完成。
vue 中 eventbus 被多次触发,在 this.$on 监听事件时,内部的 this 发生改变导致,无法在 vue 实例中添加数据。
项目场景
一开始的需求是这样的,为了实现两个组件 (A.vue ,B.vue) 之间的数据传递。页面 A,点击页面上的某个 A 上的某一个按钮之后,页面会跳转到页面 B。这个时候需要将页面 A 上的数据携带过去给页面 B。
业务代码
// 点击之后,emit 自定义事件(increment) 就会跳转到 / B 页面。接下来就是在 B 页面中 on 接受这个事件(increment)。获得这个数据。<template>
<div>
I am AChild
<button @click="increment">emit</button>
</div>
</template>
<script>
export default {
name: 'Achild',
methods: {increment() {console.log('A 触发了 $emit')
this.bus.$emit('increment', '我是 increment')
this.$router.push('/B')
}
},
</script>
<template>
<div>
I am BChild
<p>{{info}}</p>
</div>
</template>
<script>
export default {
name: 'Bchild',
data() {
return {info: 'default info'}
},
created() {
let _this = this
console.log('Bchild this', this)
this.bus.$on('increment', function(data) {console.log('这是 Bchild 收到的值:', data)
console.log('Bchild in _this:', _this)
_this.info = data
})
},
</script>
按照理论,我觉得只要在页面 A 触发了 increment 事件,页面 B 就会理所当然的接受了数据。然而,结果却不如人意。
从这里可以发现 页面 B 根本就没有接收到这个事件。
然后再从页面 B 回退到 页面 A,再重复一遍 emit increment 事件。会神奇的发现 B 竟然收到了 A 传递过来的数据。
你会发现,第一次触发事件 increment 的时候,B 并没有收到。第二次触发的时候,就输出了一个。第三次触发的时候,就又输出了两个、、、依次增加。而且你还会发现打印出的 on 的回调函数打印出的 this 指向,并不是指向当前 vue 实例 (B.vue)。而且明明是顺序执行,却偏偏是异步执行。on 的回调函数先于 console.log(‘Bchild this’, this) 执行。
- 问题 1:为什么第一次触发事件的时候,页面 B on 没有监听到事件。
- 问题 2:为什么后面再一次依次去触发的时候会出现,每一次都会发现好像之前的 on 事件分发都没有被撤销一样,导致每一次的事件触发执行越来越多。
- 问题 3:为什么是 on 里的回调函数先执行?输出的指向且并不指向当前 vue 实例?
解决
这些问题的出现还要从 vue 的生命周期讲起。可以参考我的另一篇关于 vue 生命周期的博客
从这里我们可以清楚的看到,当我们还在页面 A 触发 emit 事件时,页面 B 还没有生成,也就是说页面 B 中 created 中所监听的来自于 A 中的事件还没有被触发。这个时候你 A 中 emit 事件的时候,B 还没有监听到。
再仔细看看,当我们从 A 页面跳转到 B 页面中的时候发生了什么?首先是 B 组件 created 然后 beforeMounted 接着 A 组件才被销毁,A 组件才仔细 beforeDestory,以及 destoryed。然后 B 组件再执行 mounted。所以我们可以把 A 页面组件中的 emit 事件放到 beforeDestory 里,因为这个时候,B 组件的 created 钩子已经执行,也就可以监听到从 A 传过来的事件了。而且从周期来看,B 的 $on 监听,也不能放在 mounted 钩子里,不然也会出现监听不到的情况。
<template>
<div>
I am AChild
<button @click="increment">emit</button>
</div>
</template>
<script>
export default {
name: 'Achild',
methods: {increment() {console.log('A 触发了 $emit')
this.$router.push('/B')
}
},
beforeDestroy () {this.bus.$emit('increment', '我是 increment')
}
}
</script>
修改过后效果图:
我们可以看到修改后,B 明显可以收到 A 传递过来的数据。但是多次点击,事件的触发还是会依次增加,控制台打印的输出每次都有增加。而且每次在 $on 里的回调函数会打印出以前监听到的 vue 实例,和本次监听的实例。
总结
查找各方面资料,才知道$on 事件是不会自动销毁的。需要我们手动来销毁。
这是因为 Bus 是全局的,并不随着页面的切换而重新执行生命周期,所以 $on 能存储到以前的实例,这样看起来才比较奇怪。如果没有 A 组件没有将 emit 放在 beforeDestory 钩子里,通过全局的事件总线 bus(没有受生命周期约束),而 B 里的 $on 里没有监听到最新的 emit,只会收到以前的事件,所以 $on 的 this 会指向上次 B.vue 的 vue 实例。导致现在的 B.vue 就算看起来拿到了数据,也无法挂载到现在的 B 实例上。
所以在 B 组件添加
beforeDestroy () {this.bus.$off('increment')
}
建议
使用 bus 时一定要注意,组件的生命周期。对于这种会被销毁的 vue 实例。一定要把 emit 放在 beforeDestory 里面。并且要记得将 $on 销毁。
如果是要保存这种状态最好使用 vuex,进行数据传递。这样这些传递的值,就不会受组件的销毁新建的影响,可以保存下来。
原博客
参考博客