jsernews 1.0.1

<~>

ts 203 days ago.

原文链接:https://blog.safia.rocks/post/169618575955/node-module-deep-dive-eventemitter

所以,在我最近一个与 Node 有关的博客文章中,我深入了一些 C++ 代码细节,我想我会回到我的舒适区,阅读更多的 JavaScript 代码。

当我第一次开始学习 Node 的时候,我一度难以理解的是事件驱动的语言本质。我没有真正与事件驱动的编程语言交互。事后看来,其实我有。在学习 Node 之前,我在我的代码中使用过 jQuery 的 .on.click,这是一种事件驱动的编程风格。那时候,我并没有真正意识到我在写事件驱动的代码。无论如何,我现在好奇的一件事就是 Node 中的事件发射器。所以让我们来了解它。

如果您不熟悉 Node 的事件驱动特性,可以查看几篇博客文章,这些博文可以比我更好地解释它。这里有一些可能对你有所帮助。

好的,所以我想阅读 EventEmitter 的代码,看看我能否理解 EventEmitter 类的内部发生了什么。你可以在这里找到我将要引用的代码。

因此,任何 EventEmitter 对象中最重要的两个函数是 .on.emit.on 函数是负责监听特定类型事件的函数。.emit 函数负责调度特定类型的事件。我决定开始探索这些特定函数的代码。我将从 .emit 开始,因为在查看它们如何监听事件之前,先看看事件是如何发出的是有意义的。

如果你使用了 EventEmitter 对象,那么对于 emit 的函数声明是不言自明的。它需要一个类型参数,通常是一个字符串,以及一组将传递给处理程序的参数。

EventEmitter.prototype.emit = function emit(type, ...args) {

首先引起我注意的部分代码是“错误”类型事件和其他类型的事件处理方式不同。说实话,我花了一段时间才能仔细研究下面代码中发生的事情,特别是 if-else if这一小部分。所以基本上,这段代码所做的是检查发出的事件是否是 error 事件。如果是,它检查属于这个 EventEmitter 的监听器集合中是否有监听 error 事件的监听器。如果有监听器,函数将继续。

let doError = (type === 'error');

const events = this._events;
if (events !== undefined)
  doError = (doError && events.error === undefined);
else if (!doError)
  return false;

如果没有错误事件监听器(如注释所说),则发射器将向用户抛出错误。

// If there is no 'error' event listener then throw.
if (doError) {
  let er;
  if (args.length > 0)
    er = args[0];
  if (er instanceof Error) {
    throw er; // Unhandled 'error' event
  }
  // At least give some kind of context to the user
  const errors = lazyErrors();
  const err = new errors.Error('ERR_UNHANDLED_ERROR', er);
  err.context = er;
  throw err;
}

另一方面,如果抛出的类型不是错误,那么 emit 函数将查看附加在 EventEmitter 对象上的监听器,以查看是否为该特定 type 声明了任何侦听器并调用它们。

const handler = events[type];

if (handler === undefined)
  return false;

if (typeof handler === 'function') {
  Reflect.apply(handler, this, args);
} else {
  const len = handler.length;
  const listeners = arrayClone(handler, len);
  for (var i = 0; i < len; ++i)
    Reflect.apply(listeners[i], this, args);
}

return true;

整洁!这非常简洁明了。继续 on 函数...

EventEmitter 中的 on 函数隐式调用 _addListener 内部函数,该函数声明定义如下所示。

function _addListener(target, type, listener, prepend)

这些参数中的大部分都是不言自明的,对我来说唯一好奇的是 prepend 参数。事实证明,该参数默认为 false,开发人员无法通过任何公共 API 配置该参数。

我查找了一些 GitHub 提交消息,弄清楚了这一点。似乎在 _addListener 对象中设置为 false,是因为许多开发人员不恰当地访问 EventEmitter 对象上的内部 _events 属性,以将监听器添加到列表的开头。如果你想这样做,你应该使用 prependListener

_addListener 函数首先进行一些基本的参数验证。我们不希望任何人搬起砖头砸到了自己的脚。一旦添加了参数,函数就会尝试将 typelistener 添加到当前 EventEmitter 对象的 events 属性中。我觉得有趣的代码片段是下面的代码。

if (events === undefined) {
  events = target._events = Object.create(null);
  target._eventsCount = 0;
} else {
  // To avoid recursion in the case that type === "newListener"! Before
  // adding it to the listeners, first emit "newListener".
  if (events.newListener !== undefined) {
    target.emit('newListener', type,
                listener.listener ? listener.listener : listener);

    // Re-assign `events` because a newListener handler could have caused the
    // this._events to be assigned to a new object
    events = target._events;
  }
  existing = events[type];
}

我对这里的 else 片段特别好奇。看起来有关于事件属性是否已经在当前的 EventEmitter 对象上初始化(意味着我们之前已经添加了一个监听器),但是有一些奇怪的边缘情况检查业务正在进行。我决定做一些 GitHub 人类学来弄清楚何时添加了这个特定的代码更改,以便更深入地了解 bug 如何出现以及为什么添加。我很快意识到这是一个糟糕的主意,因为这种特殊的逻辑在代码中已经使用了大约4年,我无法追踪它的起源。我试图更仔细地阅读代码,看看究竟是什么类型的边缘情况。

我最终不是通过阅读代码,而是通过阅读文档来了解它。不要忘了吃蔬菜和阅读所有的文档,孩子们! Node 文档指出:

所有 EventEmitter 在添加新监听器时发出事件 newListener,当现有监听器被删除时发出 removeListener

所以当添加新监听器时,在实际监听器添加到 EventEmitter 上的 _events 属性之前,会发出 newListener 事件。这是因为如果你添加一个 newListener 事件监听器,并且它被添加到默认发出 newListener 事件之前的事件列表中,最终它将调用它自己。这就是为什么这个 newListener 事件发出的代码放在函数的顶部。

下一个代码尝试确定是否已经连接了此 type 的监听器。基本上,这样做的目的是确保如果事件只有一个监听器,那么它将被设置为 _events 关联数组中的函数值。如果不止一个监听器,则将其设置为数组。这只是一个小小的优化,但许多这类辅助的优化让 Node 变得非常棒!

if (existing === undefined) {
  // Optimize the case of one listener. Don't need the extra array object.
  existing = events[type] = listener;
  ++target._eventsCount;
} else {
  if (typeof existing === 'function') {
    // Adding the second element, need to change to array.
    existing = events[type] =
      prepend ? [listener, existing] : [existing, listener];
    // If we've already got an array, just append.
  } else if (prepend) {
    existing.unshift(listener);
  } else {
    existing.push(listener);
  }

在此函数中进行的最后一次检查尝试确认特定事件发射器上的特定事件类型是否连接了太多监听器。如果是这种情况,则可能意味着代码中存在错误。总的来说,我认为将许多监听器连接到单个事件并不是一个好习惯,因此 Node 会进行一些有用的检查以警告您是否正在执行此操作。

  // Check for listener leak
  if (!existing.warned) {
    m = $getMaxListeners(target);
    if (m && m > 0 && existing.length > m) {
      existing.warned = true;
      // No error code for this since it is a Warning
      const w = new Error('Possible EventEmitter memory leak detected. ' +
                          `${existing.length} ${String(type)} listeners ` +
                          'added. Use emitter.setMaxListeners() to ' +
                          'increase limit');
      w.name = 'MaxListenersExceededWarning';
      w.emitter = target;
      w.type = type;
      w.count = existing.length;
      process.emitWarning(w);
    }
  }
}

就是这样!在所有这些结束时,这个 .on 函数返回它所附属的 EventEmitter 对象。

我真的很喜欢阅读 EventEmitter 的代码。我发现它非常清晰和平易近人(不像我上一次进行的 C++ 冒险) - 尽管我怀疑这一点与我熟悉的语言有一定的关系。