深色模式
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-step、tools/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 的对应
| 本 Demo | dsh 里的真实事件 |
|---|---|
greet(emit) | session/event、agent/status |
calculate(waterfall) | agent/pre-step、agent/request、llm/stream、tools/pre-execute |
whoCan(serial) | agent/turn-stopping |
下一步
Demo 3 把"服务"与"作用域"的机制跑起来。