Skip to content

Demo 2 · 事件总线的四种分发模式

目标:把 emit / waterfall / parallel / serial 的语义差异跑出来,并亲手踩两个经典 bug。对应原理篇 02.5。代码:demos/02-events/main.ts

运行

sh
cd demos
npm run demo:2

输出解读(逐段)

① emit:同步广播

text
── 1. emit:同步广播,不等待监听者 ──
  监听器 A 收到 tick #1
  监听器 B 收到 tick #1

顺序永远是注册顺序(A 在 B 前),emit 同步返回,不等待任何异步完成。用途:通知/广播事实——dsh 的 session/event 就是 emit:会话日志追加后广播给所有消费者。

② waterfall:环绕式中间件

text
── 2. waterfall:监听器包裹 next() ──
  [审计] 计算请求:1 + 1
  [审计] 计算结果:2
  waterfall('1 + 1') = 2
  [审计] 计算请求:evil()
  [策略] 检测到非法表达式,短路!
  [审计] 计算结果:-1
  waterfall('evil()') = -1  ← 内置行为从未执行

重点看第二组:策略监听器没有调 next(),直接返回 -1。内置行为(返回 2 的那个)根本没执行。这就是"否决"——dsh 里 agent/pre-steptools/pre-execute 都用这个语义实现"拒绝"。

③ parallel:并发扇出

text
── 3. parallel:并发执行并等待全部 ──
  [快速] 完成 2 个 URL
  [慢速] 完成 2 个 URL
  总耗时 302ms(并发 ≈ 300ms,串行要 350ms)

"快速"先完成(50ms),"慢速"后完成(300ms),总耗时取最长者而非相加。用途:扇出后等齐

④ serial:顺序执行直到有人拍板

text
── 4. serial:依次询问,直到有人 bail ──
  [翻译官] 我会翻译,但这题问我?不会。
  [工程师] debug TypeScript?我会!
  最终接单者:engineer(厨师从未被询问)

"厨师"从未被询问——engineer 返回非空值(bail)后链条停止。用途:按序征询决策——dsh 的 agent/turn-stopping 就是 serial 的最后检查点。

⑤ 两个经典 bug

text
── 5. 经典 bug:waterfall 忘了调 next() ──
  [bug 监听器] 只想记录 2 + 2,但忘了 next()…
  结果 = 0(预期 4,实际被 bug 监听器短路成 0)

只读监听忘了委托 → 整条链短路。规则:只读监听必须调 next();只有明确要否决时才短路。

text
── 5b. 经典 bug:emit 监听器抛错 ──
  emit 同步抛出: 我崩了

emit 是同步的,监听器异常直接炸穿派发。demo 里用 on() 返回的 disposer 手动移除坏监听器——disposer 是 ctx.on 的返回值,这本身就是可逆注册的体现

⑥ 卸载 = 移除监听器

临时插件被 registry.delete 后,它的 tick 监听器不再出现——监听器是 effect,随 fiber 走

亲手做实验

实验 1:把 ⑤ 的 bug 监听器改对

在 bug 监听器里补上 return next(),观察结果恢复为 4。

实验 2:waterfall 里换返回值

写一个监听器:调 next() 拿到下游结果后 return 结果 * 10。体会"包裹与改写"——dsh 的 agent/request 就是这么替换请求配置的。

实验 3:prepend 参数

把两个 tick 监听器之一注册为 ctx.on('tick', fn, { prepend: true }),观察顺序变化。什么时候需要 prepend?想想"策略必须在普通监听器之前"的场景。

与 dsh 的对应

本 Demodsh 里的真实事件
greet(emit)session/eventagent/status
calculate(waterfall)agent/pre-stepagent/requestllm/streamtools/pre-execute
whoCan(serial)agent/turn-stopping

下一步

Demo 3 把"服务"与"作用域"的机制跑起来。

基于 DeepSeek Harness(开发者预览版 0.1.0-rc.6)与 Cordis 撰写