事件循环与 EventEmitter

专栏:TypeScript 与 Node 地基 · 第 5 / 13 篇
TypeScriptNode事件循环

:::info 学习目标 完成本篇后你能够:解释”单线程为什么不卡”;区分微任务与宏任务的执行顺序;用 EventEmitter 实现发布-订阅;理解 typed events 的类型安全写法。 前置:第 3 篇完成。预计时长:60 分钟。 :::

Node 用一个线程服务成千上万的并发请求,靠的是事件循环:所有耗时操作都委托给系统(网络/磁盘)或定时器,完成后回调被排进队列,由事件循环依次执行。你的代码永远只是”处理一个事件”,处理完立刻让出线程。

执行顺序:同步 → 微任务 → 宏任务

console.log('1 同步')
setTimeout(() => console.log('4 宏任务:setTimeout'), 0)
Promise.resolve().then(() => console.log('3 微任务:then'))
console.log('2 同步')
// 输出:1 2 3 4

规则:同步代码先跑完 → 清空全部微任务(Promise.then、queueMicrotask)→ 执行一个宏任务(setTimeout、IO 回调)→ 再清空微任务 → 循环。await 的续体也是微任务——这解释了上一篇所有关于执行顺序的直觉。

EventEmitter:发布-订阅模式

Node 的 EventEmitter 是事件系统的基础组件:一个”频道”(事件名),多个”听众”(回调),发射时按注册顺序通知:

import { EventEmitter } from 'node:events'

const bus = new EventEmitter()
bus.on('message', (msg) => console.log('收到:', msg))   // 订阅
bus.emit('message', '你好')                             // 发布

Agent 场景天然适合这个模式:会话里发生的每件事(用户消息、工具调用、轮次结束)都是事件,UI、日志、计费都是订阅者——它们互不认识,却都能响应同一份事件流。

带类型的 EventEmitter:Cordis 事件系统的雏形

裸 EventEmitter 的事件名和参数都没有类型检查。dsh 的做法(也是 LangGraph、Node 生态的常见做法):用声明合并给事件名和参数建立类型映射(下一篇详讲声明合并,先看用法):

interface Events {
  'user/message'(content: string): void
  'tool/call'(name: string, args: unknown): void
}

// 强类型总线:事件名拼错、参数个数不对都编译报错
bus.on('tool/call', (name, args) => { ... })    // ✓
bus.on('tool/cal', () => {})                    // ✗ 编译错误:拼错的事件名

dsh 的 Cordis 在这之上加了三种分发模式(第 4 篇 dsh 专栏详述):emit(广播)、waterfall(洋葱式层层加工)、serial(依次裁决)——本篇的 EventEmitter 就是它们的原点。

一次事件的处理流程

图表(ts-event-loop-emitter.md)

与其他主题的联系

  • 第 3 篇的 await 在底层就是微任务——两个异步知识其实是一体的;
  • dsh 的 session/eventtools/result 都是本篇模式的工程化版本(多了类型、作用域过滤与分发模式);
  • 第 7 篇的插件宿主会亲手实现一个迷你事件总线。

常见踩坑

  1. 同步代码阻塞事件循环——一个 while(true) 或超大 JSON.parse 会让所有请求卡死;重计算放 worker;
  2. 忘掉 emit 是同步的——emit立即调用所有监听器(不等事件循环),监听器里的异常会传播到发射点;
  3. 监听器抛错没有兜底——给 EventEmitter 加 error 事件或 try/catch,否则一个坏监听器拖垮全局;
  4. 监听器泄漏——on 了不 off,长驻进程内存缓慢上涨。once 或保存 disposer。

随堂练习(带验收标准)

  1. 实现一个 TypedEmitter<Events> 泛型类:on/emit 的事件名与参数由泛型约束。验收:拼错事件名编译报错;
  2. 写三段代码验证执行顺序(同步/微任务/宏任务)。验收:不看运行结果就能预测输出;
  3. packages/core/session/src/index.tssession/event 的发射处,对照本篇理解”谁在 emit、谁在 on”。

← 返回文章列表