项目背景

  1. 版本:vue-cli 2.x
  2. 用途:兄弟组件传递参数,连环嵌套解单方法耦合
  3. 方法:bus(或者叫eventBus),通过import一个统一的bus.js文件,来对一个新的vue实例进行事件绑定,从而做到状态传递。
  4. bus.js
  5. import Vue from 'vue'
  6. export default new Vue();
  7. 为什么不用vuex:项目本身组件关系不算复杂,基本不存在组件间大量修改状态

事件背景

  1. 在这个项目进入页面之前,需要通过jsBridge获取一系列客户端数据。
  2. 于是我在主页面组件中使用$emit来触发事件:
  1. import Bus from "@/assets/js/bus"
  2. export default {
  3. mounted() {
  4. Bus.$emit("initAppVersion", this.getData)
  5. }
  6. }
  1. 然后在总的入口组件中,使用$on来接收:
  1. import Bus from "@/assets/js/bus";
  2. export default {
  3. name: 'App',
  4. beforeCreate() {
  5. //获取初始值
  6. Bus.$on("initAppVersion",(callback) => {
  7. this.getAppVersion(
  8. (res) => {
  9. Bus.$emit("initAppChanne", callback)
  10. },
  11. () => {
  12. Bus.$emit("initAppChanne", callback)
  13. });
  14. });
  15. Bus.$on("initAppChannel",(callback)=> {
  16. this.getChannel(
  17. (res) => {
  18. Bus.$emit("initUserinfo",callback)
  19. },
  20. () => {
  21. Bus.$emit("initUserinfo",callback)
  22. });
  23. });
  24. Bus.$on("initUserinfo", (callback) => {
  25. this.getUserInfo(
  26. (res) => {
  27. callback && callback()
  28. },
  29. () => {
  30. callback && callback()
  31. })
  32. })
  33. }
  1. 其实就是用回调,保证几个异步进行的方法getAppVersion、getChannel、getUserInfo按顺序进行,并且都执行完之后再调用全局数据初始化的接口请求。并且保证在调用组件中代码的整洁明了。
  2. 一开始调试非常的正常,直到最后所有代码放开走正常流程,发现,在npm run dev,也就是运行webpack-dev-server —inline —progress —config build/webpack.dev.conf.js,并且进行了热更新之后,会重复调用$on的命令,并且,这三个嵌套的命令会被指数级调用,第一个命令3次,第二个就9次,第三个就27次,这会直接导致刷接口的效果。
  3. 但是神奇的是,首次进入页面却没有这个问题,只有热更新之后才会存在。

解决方法:

  1. 将触发器的$on改为$once,即会在第一次触发之后移除监听器
  1. Bus.$once("initUserinfo", (callback) => {
  2. this.getUserInfo((res) => {
  3. callback && callback()
  4. }, () => {
  5. callback && callback()
  6. })
  7. })
  1. 在最后一个回调触发的最后,添加Bus.$off清除所有的监听器
  1. Bus.$on("initUserinfo", (callback) => {
  2. this.getUserInfo((res) => {
  3. callback && callback()
  4. }, () => {
  5. callback && callback()
  6. Bus.$off()
  7. })
  8. })

解决过程

问题

  1. $emit,$on到底干了什么
  2. webpack热更新的过程经历了哪几个生命周期?

解答

  1. $emit,$on到底干了什么
  • 对应部分源码可见:vue/src/core/instance/events.js
    可知,基本为:
    1. 使用$on为对应实例的_event添加一个可触发的自定义事件。
    2. 使用$emit寻找是否有这个时间,有的话就触发当前实例上的对应事件。
    3. 并且,通过console我们可以知道,_event中的事件是以监听的事件名为key,存储的方法为数组形式,当多次进行$on时,只会往对应key值的数组中进行push,而不是重写。
    4. 而当进行了多次$on时,再进行$emit时,就会一次触发所有写在这个事件里的$on方法。
  • demo:
  1. Bus.$on('test', () => {
  2. console.log('1')
  3. })
  4. Bus.$on('test', () => {
  5. console.log('2')
  6. })
  7. console.log(Bus._events)
  8. Bus.$emit('test')

屏幕快照 2019-08-11 下午5.17.04.png

  • 可以看到,1和2都打印出来了,这就是指数级触发的原因。
  1. webpack热更新的过程经历了哪几个生命周期?
  • 通过一个最简单粗暴的方式,我们可以看出热更新的过程中,vue经历了从beforeCreate -> created -> beforeMount -> mounted的完整周期。
  1. beforeCreate() {
  2. console.log('before-create')
  3. },
  4. created() {
  5. console.log('create')
  6. },
  7. beforeMount() {
  8. console.log('before-mount')
  9. },
  10. mounted() {
  11. console.log('mount1')
  12. }

屏幕快照 2019-08-11 下午5.31.02.png

总结问题来源

  • 综上所述,本次问题即是因为热更新时不会像进入页面时那样销毁整个文件并且重新加载,导致身处beforeCreate()中的Bus.$on重新绑定,从而导致重复触发。
  • 我使用的解决方法正如上文,在完成所有的Bus.$on事件之后使用Bus.$off进行事件解绑,并且因为beforeCreate周期会重复触发,即使是热更新也依旧是完整的代码逻辑。
  • 当然,在整理的过程中,也发现了一个更加简便的方式,那就是不使用$on而是使用$once,它的官方解释是“监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器”,正适合本次这样的使用场景。