很多前端开发者在做组件之间通信的时候,都会用到类似observers订阅、triggerEvent触发事件的模式——比如大家熟悉的Vue事件总线、甚至React的自定义事件,都是这类模式的变种。不过用起来爽的同时,藏着不少容易忽略的小陷阱,踩中了就会出现一些莫名其妙的异常,比如弹窗点一次关不掉、按钮点击没反应、页面越来越卡这些问题,今天就来唠唠这些隐藏的坑,再给大家说怎么避。

一、自定义组件通信的日常用法

这个模式其实特别好理解,你可以把事件总线当成一个“公共聊天群”:想要收消息的组件,就发个“我要进群,收到XX消息记得告诉我”的请求(对应observers订阅);想要发消息的组件,就往群里喊一声“我发了XX消息”(对应triggerEvent触发),这样不管是父子组件、还是没直接关联的兄弟组件,都能通过这个群传消息,不用一层一层麻烦地传参数。

1.1 基础用法示例

这里用大家最熟悉的Vue2技术栈来举例,代码都是日常开发里常写的:

// 技术栈:Vue2
import Vue from 'vue';
// 建一个全局的事件总线,相当于一个固定的公共聊天群
const eventBus = new Vue();
export default eventBus;

第一个子组件想收消息,就用订阅方法:

// 子组件A.vue(负责收消息)
import eventBus from './eventBus.js';
export default {
  name: 'ComponentA',
  created() { // 组件刚建好的时候
    // 订阅名为updateData的消息,收到消息就执行对应的逻辑
    eventBus.$on('updateData', (data) => {
      console.log('我收到了数据:', data);
      // 这里可以写组件要做的事,比如更新界面、调用接口
    });
  }
};

另一个子组件想发消息,就用触发方法:

// 子组件B.vue(负责发消息)
import eventBus from './eventBus.js';
export default {
  name: 'ComponentB',
  methods: {
    sendMessage() { // 点按钮就触发消息
      // 往群里喊一声:我发了updateData消息,带的数据是"我是B传的"
      eventBus.$emit('updateData', '我是B发的测试数据');
    }
  }
};

二、那些容易踩的隐式陷阱

很多新手用这个模式,只会抄上面的代码,没注意到细节,等到出现异常才摸不着头脑。最常见的坑有两个,都是肉眼不容易发现的:

2.1 忘了解绑导致的重复执行

比如子组件A是一个弹窗,每次打开弹窗都会创建新的组件实例,每次实例都订阅了消息,但当用户关闭弹窗(组件被销毁)的时候,之前的订阅记录还留在聊天群里,没被删掉。等下次再打开弹窗,新的实例又订阅一次,这个群里就有了两条一模一样的订阅记录,再触发一次消息,就会执行两次逻辑——比如弹出两次提示、调用两次接口,明显不对,但又找不到哪里错了。

2.2 事件名写错导致的“假死”

这个坑更隐蔽,比如组件A订阅的是filterChange消息,组件B触发的是filterChanged消息,就差了一个字母,这时候群里的订阅者(组件A)根本听不到消息,组件B喊破喉咙也没人理,调试半天都以为是通信逻辑错了,其实只是手输错了事件名。

三、陷阱场景的详细示例

咱们拿一个实际的搜索页面举例:页面有三个组件,搜索框、筛选栏、结果列表,筛选栏负责选条件,选完后要通知结果列表更新内容,三个组件都用刚才的事件总线通信。

3.1 重复触发的具体场景(错误示例)

筛选栏组件每次打开都会订阅筛选消息,但没做解绑,就会出现重复执行:

// 错误的筛选栏组件,没解绑事件
import eventBus from './eventBus.js';
export default {
  name: 'FilterBar',
  created() { // 组件刚建好就订阅
    // 订阅筛选变化的消息,收到就执行筛选逻辑
    eventBus.$on('filterChange', (params) => {
      console.log('筛选参数是:', params);
      // 这里要调用接口拿结果,要是重复触发就会调用两次接口
    });
  }
};

当用户第一次打开筛选栏,订阅了一条消息;关掉筛选栏(组件销毁),但这条订阅还在群里;第二次打开筛选栏,又多了一条订阅。之后用户选条件,发送一次消息,群里就有两条相同的订阅,结果就是执行两次筛选逻辑,明显不对。

3.2 事件名写错的具体场景

刚才的例子里,筛选栏订阅的是filterChange,结果列表组件触发的是filterChanged,少了一个e,结果列表收不到消息,用户选完条件,结果页根本不更新,调试的时候会以为是哪里的逻辑错了,其实只是事件名打错了。

四、避坑指南

知道了陷阱的原因,避坑就很简单,记住三个关键点就行,都是日常开发就能做到的事。

4.1 组件销毁时一定要解绑事件

当组件快要消失(Vue里的beforeDestroy阶段),一定要把对应的订阅删掉,而且要注意,解绑的时候要和订阅时用同一个逻辑函数,不能用匿名函数,不然找不到对应的订阅记录,删不掉。

// 正确的筛选栏组件,带解绑逻辑
import eventBus from './eventBus.js';
export default {
  name: 'FilterBar',
  created() {
    // 把要执行的逻辑存成变量,方便后面解绑用
    this.filterLogic = (params) => {
      console.log('筛选参数是:', params);
      // 执行筛选逻辑
    };
    eventBus.$on('filterChange', this.filterLogic);
  },
  beforeDestroy() { // 组件快要消失时解绑
    // 解绑对应事件,参数要和订阅时完全一样
    eventBus.$off('filterChange', this.filterLogic);
  }
};

4.2 事件名统一管理,避免手写错误

把所有事件名写在一个单独的常量文件里,不用每次手动打字,这样就不会写错,哪怕改事件名只要改一次就行,特别方便:

// 专门存事件名的文件eventNames.js,全是常量,不会写错
export const FILTER_CHANGE = 'filterChange';
export const UPDATE_DATA = 'updateData';

然后组件里引用这个常量,就不用担心打错了:

// 筛选栏组件正确用法,用常量代替手写
import eventBus from './eventBus.js';
import { FILTER_CHANGE } from './eventNames.js';
export default {
  created() {
    this.filterLogic = (params) => { /* 筛选逻辑 */ };
    eventBus.$on(FILTER_CHANGE, this.filterLogic);
  },
  beforeDestroy() {
    eventBus.$off(FILTER_CHANGE, this.filterLogic);
  }
};

4.3 按需订阅,不要乱绑定

只在确实需要的组件里订阅事件,不要在公共组件、通用组件里随便加订阅,减少不必要的事件绑定,也能降低陷阱出现的概率。

五、总结

用observers订阅、triggerEvent触发的模式做组件通信,确实很方便,尤其是非父子组件之间传值,不用一层一层传参数,节省很多重复代码。但这个模式的隐式陷阱也不少,最常见的就是没解绑导致的重复执行,还有手误写错事件名导致的通信失效。只要记住两个核心原则:组件销毁时一定解绑对应事件,所有事件名统一管理,就能避开大多数坑,让你的组件通信代码更稳定,不会出现那些莫名其妙的异常问题。