Skip to content

Code Analysis EventEmitter3

Tu Shaokun edited this page Nov 5, 2026 · 1 revision

两个同名监听器,实际运行哪一个?

在 EventEmitter3 5.0.1 的固定版本上,先从源码写下两次分派的预期,再运行 Node 和 Code Analysis。这个案例检验一个版本中的行为。固定源码

问题 源码和分析结果
注册走哪条路径? index.js:228 与 :241 的 on 和 once 都调用第 58 行的 addListener。第 63 行创建监听记录,第 42 行保存具体函数。公开 Labs bf0a29e7 能定位这些函数,对该文件给出 0 条调用记录。本地实验 c3dddd5c 把两个注册调用都指到了第 58 行。
实际分派在哪里? emit 在第 166–216 行。本地实验在其中标出 14 个调用位置。对照源码,有 12 处通过 .fn.call 或 .fn.apply 执行保存的监听器,另 2 处负责移除一次性注册。12 处均标作目标未解析。
同名会导致误认吗? 同一发射器按事件选出注册记录,再按函数引用、上下文和一次性标记处理。同名闭包可以是不同的对象。保留调用位置,让调查者回查注册代码,比仅用名字选择目标可靠。

读到分派处的 .fn 后,沿字段赋值、记录构造和两个注册入口回查,可以在一个文件内还原这条路径。下一步在注册与分派跨文件的真实变更上检验同样的做法。按上下文移除的上游测试

复现两次分派

将 EventEmitter3 检出到 a650549176ded0d0dcc52e27b03ae5669d7dcd7c,在该仓库根目录用 Node 22 运行下列代码。它只读取源码入口。A、B 都叫 handler,A 在两个上下文各注册一次,B 只执行一次。

node <<'JS'
const assert = require('node:assert/strict');
const EE = require('./index.js');
const seen = [];
function make(id) {
  return function handler() { seen.push(id + '@' + this.id); };
}
const a = make('A'), b = make('B'), decoy = make('unexpected');
const x = { id: 'X' }, y = { id: 'Y' }, z = { id: 'Z' };
const e = new EE();
e.on('tick', a, x).on('tick', a, y).once('tick', b, z);
e.on('other', decoy, z);
new EE().on('tick', decoy, z);
assert.equal(a.name, b.name);
assert.notEqual(a, b);
e.removeListener('tick', make('unused'), x);
assert.equal(e.listenerCount('tick'), 3);
e.emit('tick');
const first = seen.splice(0);
e.removeListener('tick', a, x);
e.emit('tick');
assert.deepEqual(first, ['A@X', 'A@Y', 'B@Z']);
assert.deepEqual(seen, ['A@Y']);
console.log(first, seen);
JS

返回团队验证记录

Clone this wiki locally