003:能不能简单实现一下 node 中回调函数的机制?

回调函数的方式其实内部利用了发布-订阅模式,在这里我们以模拟实现 node 中的 Event 模块为例来写实现回调函数的机制。

function EventEmitter() {
  this.events = new Map();
}

这个 EventEmitter 一共需要实现这些方法: addListener, removeListener, once, removeAllListener, emit

首先是addListener:

// once 参数表示是否只是触发一次
const wrapCallback = (fn, once = false) => ({ callback: fn, once });

EventEmitter.prototype.addListener = function (type, fn, once = false) {
  let handler = this.events.get(type);
  if (!handler) {
    // 为 type 事件绑定回调
    this.events.set(type, wrapCallback(fn, once));
  } else if (handler && typeof handler.callback === 'function') {
    // 目前 type 事件只有一个回调
    this.events.set(type, [handler, wrapCallback(fn, once)]);
  } else {
    // 目前 type 事件回调数 >= 2
    handler.push(wrapCallback(fn, once));
  }
}

removeLisener 的实现如下:

EventEmitter.prototype.removeListener = function (type, listener) {
  let handler = this.events.get(type);
  if (!handler) return;
  if (!Array.isArray(handler)) {
    if (handler.callback === listener.callback) this.events.delete(type);
    else return;
  }
  for (let i = 0; i < handler.length; i++) {
    let item = handler[i];
    if (item.callback === listener.callback) {
      // 删除该回调,注意数组塌陷的问题,即后面的元素会往前挪一位。i 要 -- 
      handler.splice(i, 1);
      i--;
      if (handler.length === 1) {
        // 长度为 1 就不用数组存了
        this.events.set(type, handler[0]);
      }
    }
  }
}

once 实现思路很简单,先调用 addListener 添加上了once标记的回调对象, 然后在 emit 的时候遍历回调列表,将标记了once: true的项remove掉即可。

EventEmitter.prototype.once = function (type, fn) {
  this.addListener(type, fn, true);
}

EventEmitter.prototype.emit = function (type, ...args) {
  let handler = this.events.get(type);
  if (!handler) return;
  if (Array.isArray(handler)) {
    // 遍历列表,执行回调
    handler.map(item => {
      item.callback.apply(this, args);
      // 标记的 once: true 的项直接移除
      if (item.once) this.removeListener(type, item);
    })
  } else {
    // 只有一个回调则直接执行
    handler.callback.apply(this, args);
  }
  return true;
}

最后是 removeAllListener:

EventEmitter.prototype.removeAllListener = function (type) {
  let handler = this.events.get(type);
  if (!handler) return;
  else this.events.delete(type);
}

现在我们测试一下:

let e = new EventEmitter();
e.addListener('type', () => {
  console.log("type事件触发!");
})
e.addListener('type', () => {
  console.log("WOW!type事件又触发了!");
})

function f() { 
  console.log("type事件我只触发一次"); 
}
e.once('type', f)
e.emit('type');
e.emit('type');
e.removeAllListener('type');
e.emit('type');

// type事件触发!
// WOW!type事件又触发了!
// type事件我只触发一次
// type事件触发!
// WOW!type事件又触发了!

OK,一个简易的 Event 就这样实现完成了,为什么说它简易呢?因为还有很多细节的部分没有考虑:

  1. 参数少的情况下,call 的性能优于 apply,反之 apply 的性能更好。因此在执行回调时候可以根据情况调用 call 或者 apply。
  2. 考虑到内存容量,应该设置回调列表的最大值,当超过最大值的时候,应该选择部分回调进行删除操作。
  3. 鲁棒性有待提高。对于参数的校验很多地方直接忽略掉了。

不过,这个案例的目的只是带大家掌握核心的原理,如果在这里洋洋洒洒写三四百行意义也不大,有兴趣的可以去看看Node中 Event 模块 的源码,里面对各种细节和边界情况做了详细的处理。

微信公众号: 前端三元同学

获取更多资料/联系加交流群