# 发布订阅

做前端开发无论用什么框架, 可能都需要涉及到组件间的通信,除了使用属性传递,事件回调,依赖收集,注入,全局状态管理对象(Vuex,Redux)等各自框架自带的方式外,挂载一个全局的事件总线也是可以的,而且可能是更通用的做法,和框架无关。 这个全局的事件总线对象简单点可以就是一个对象,也可以是实现事件绑定、触发、移除的一个对象。

# 1、全局对象思路

这种相对简单, 可以用在暴露组件的一些私有属性,比如一个组件A有一个属性叫做startData,有很多组件都要根据组件A的开始日期做一些查询的工作。我们可以这么做

cacheObj.js

const CacheObj = {
  getStartDate: () => null
};

export { CacheObj };

然后组件A可以覆写这个方法,以下仅为示例代码, 可以是Vue的某个data属性, 也可以React的某个state

componentA.js

import { CacheObj } from './CacheObj.js';

let startDate = '20231201';
CacheObj.getStartDate = function () {
  return startDate;
};

其他的组件就可以直接使用了,比如

other.js

import { CacheObj } from './CacheObj.js';
const startDate = CacheObj.getStartDate();
fetch(url, { startDate });

WARNING

当然这个有一个问题, 就是覆写的那个组件必须先于其他的组件加载, 提前覆写好函数。除非这个返回值有一个默认值。

这种方式相对来说, 还是比较容易根据线索找到设定的逻辑的。 也可以用这种方式直接影响另外组件要触发的操作。

# 2、全局事件总线

先提供一个全局事件总线的案例,当然这个东西网上一大堆,我写的肯定不是最完美的。只能说实现基本的绑定和触发没有问题。

class EventEmitter {
  constructor() {
    this.map = {};
  }
  on(eventName, fn) {
    if (!eventName || typeof eventName !== 'string') {
      throw new Error('eventName expect a String');
      return;
    }
    if (typeof fn !== 'function') {
      throw new Error('callback expect a function');
      return;
    }
    if (this.map[eventName] === undefined) {
      this.map[eventName] = [];
    }
    this.map[eventName].push(fn);
  }
  off(eventName, fn) {
    if (!eventName || typeof eventName !== 'string') {
      throw new Error('eventName expect a String');
      return;
    }
    const fns = this.map[eventName];
    if (fns && fns.length > 0) {
      if (!fn) {
        this.map[eventName] = undefined;
        delete this.map[eventName];
      } else {
        for (let i = fns.length - 1; i >= 0; i--) {
          if (fns[i] === fn) {
            fns.splice(i, 1);
            // break;
          }
        }
      }
      if (fns.length === 0) {
        this.map[eventName] = undefined;
        delete this.map[eventName];
      }
    }
  }
  clear() {
    this.map = {};
  }
  // 判断是否有订阅某一个事件
  has(eventName) {
    return this.map[eventName] && this.map[eventName].length > 0;
  }
  emit(eventName, ...args) {
    if (!eventName || typeof eventName !== 'string') {
      throw new Error('eventName expect a String');
      return;
    }
    const fns = this.map[eventName];
    if (fns && fns.length > 0) {
      fns.forEach(fn => {
        fn(...args);
      });
    } else {
      // throw new Error(eventName + ' is not register');
    }
  }
}
const emitter = new EventEmitter();

export default emitter;

// 通知多次,直到有一次成功了。主要用在有时候事件还没绑定就开始通知 导致接收不到。
export function emitTimes(eventName, times = 5, ...args) {
  let index = 1;
  const notify = () => {
    if (emitter.has(eventName)) {
      emitter.emit(eventName, ...args);
      return;
    }
    if (index >= times) {
      return;
    }
    setTimeout(() => {
      index++;
      notify();
    }, 1000);
  };

  return notify();
}

用法

import emitter from 'emitter.js';

function onChange(val) {
  console.log(val);
}
// 绑定
emitter.on('on-change', onChange);
// 触发
emitter.emit('on-change', 'x');
// 注销
emitter.off('on-change', onChange);
// 判断是否有绑定on-change事件
emitter.has('on-change');
// 清空所有注册的事件
emitter.clear()

WARNING

这种方式在组件间通信也要注意事件绑定的先后问题。需要规避还没有绑定就触发事件, 导致事件丢失。

为了尽量避免上述的情况,提供了 emitTimes 函数。这个函数会先判断该有的事件是否有绑定, 如果没有, 过1秒再试,可以自定义重试的次数。能有效避免因为某些异步问题,组件加载顺序问题导致的通知不同步问题。

用法如下

import { emitTimes } from 'emitter.js';
emitTimes('on-change', 10, 'a'); // 尝试触发on-change事件,最多尝试10次,一共需要大概10秒
上次更新: 1/22/2025, 9:39:13 AM