深色模式
源码拆解 02 · Cordis 内核:Context 代理与 Fiber
本篇拆解 vendored Cordis 的核心机制。源码位于
vendor/cordis/src/(约 2700 行),我们只读四个文件:context.ts、events.ts、fiber.ts、reflect.ts。读完你会明白第 2 章的五个概念在代码里到底是什么。
2.1 Context:一个被代理的对象
context.ts 里最重要的三行(第 74–75 行):
ts
constructor() {
// …初始化 isolate/intercept 映射…
const self = new Proxy<this>(this, ReflectService.handler)
this.root = self
// …
return self
}Context 的运行时形态就是一个 Proxy。它只做一件事:把属性读取转交给 ReflectService.handler(reflect.ts 第 135 行):
ts
static handler: ProxyHandler<Context> = {
get(target, prop, ctx) {
if (typeof prop === 'string' && !(prop in target)) {
return getTraceable(ctx, Reflect.get(target, prop, ctx))
}
return getTraceable(ctx, Reflect.get(target, prop, ctx))
},
}翻译成白话:读 ctx.llm 时,如果 ctx 上还没有这个属性,就按名字去服务注册表里找。这就是"其他插件通过 key 查找服务,而非 import 实现"的代码级实现——ctx.llm 不是一个早就在那儿的字段,而是"此刻恰好有人提供 llm 服务"的解析结果。
三个派生操作(context.ts 第 99–145 行)也都只是"造一个新的轻量对象":
ts
extend(meta = {}) {
// Object.create:原型继承父上下文的一切,meta 成为自有属性遮蔽继承值
const self = Object.create(getTraceable(this, this))
for (const prop of Reflect.ownKeys(meta)) { /* 拷贝 meta */ }
return self
}
isolate(name: string, label?: symbol) {
const shadow = Object.create(this[symbols.isolate]) // 原型链拷贝隔离表
shadow[name] = label ?? Symbol(name)
return this.extend({ [symbols.isolate]: shadow })
}
intercept(name: string, config: any) {
const intercept = Object.create(this[symbols.intercept])
intercept[name] = config
return this.extend({ [symbols.intercept]: intercept })
}值得注意的实现选择:子上下文用原型链(Object.create)而不是深拷贝。派生一个 agent 的作用域上下文,开销是一个对象分配——这也解释了为什么 dsh 敢给每个 agent、甚至每个子代理建独立上下文。
2.2 事件总线:五模式的真实代码
events.ts 的分发实现短得惊人,值得整段读:
ts
emit(...args) {
this.dispatch('emit', args).map(cb => cb(...args)) // 同步广播,忽略返回值
}
async serial(...args) {
for (const cb of this.dispatch('serial', args)) { // 顺序执行
const result = await cb(...args)
if (isBailed(result)) return result // 非 null/false/undefined 即"拍板"
}
}
waterfall(...args) {
const cbs = this.dispatch('waterfall', args)
const inner = args.pop() // 最后一个参数:内置行为
const next = () => { const cb = cbs.shift() ?? inner; return cb(...args) }
args.push(next)
return next() // 从最外层开始调用
}waterfall 的实现就是教科书式的中间件链:next() 每次从队列头部弹一个监听器调用,队列空了就落到 inner(内置行为)。不调 next() = 链条在这里断掉 = 否决。
再看监听器注册(第 288–301 行),注意它怎么落实"注册即 effect":
ts
on(name, listener, options) {
this.ctx.fiber.assertActive() // 已卸载的 fiber 不能注册
listener = this.ctx.reflect.bind(listener) // 绑定反射追踪
const result = this.bail(this.ctx, 'internal/listener', name, listener, options)
if (result) return result // 框架内部事件有特殊拦截
const hooks = this._hooks[name] ||= []
const label = `ctx.on(${JSON.stringify(name)})`
return this.register(label, hooks, listener, options)
}
// register() 内部:
// this.ctx.fiber.effect(() => { hooks.push(record); return () => unregister(...) }, label)监听器被作为 fiber effect 登记:插入钩子表的同时返回一个"从钩子表移除"的 disposer。fiber 卸载时按逆序执行所有 disposer——第 2 章说的"自动移除"就是这么来的。label 还会出现在 fiber 诊断树(getEffects())里,热重载出问题时能看到"到底是谁没拆干净"。
2.3 Fiber:effect 的账本
fiber.ts 是生命周期的核心。每个插件实例对应一个 fiber,状态机(PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED)驱动着"等依赖、执行、服务、拆解"。
effect 的实现(第 415 行起,节选):
ts
effect(execute: () => Effect, label = 'anonymous') {
// …执行 execute(),收集它返回的 disposer(可同步/异步/生成器)
const wrapper = () => {
// 幂等:重复调用无副作用
this._disposables.delete(dispose)
return task ? disposeAfter(task) : dispose()
}
this._disposables.push(wrapper) // 按注册顺序入账
return wrapper
}
// 卸载时(第 676 行附近):
await Promise.all(this._disposables.clear().map(async (dispose) => { /* 执行 */ }))逆序来自 DisposableList 的清空顺序(后注册的先拆)。所以"先开连接、再挂文件锁"的插件,卸载时文件锁先放、连接后关——清理顺序天然正确。
生成器 effect 值得一提:如果 effect 体是 async generator,每 yield 一个 disposer 就立刻入账。这让"边注册边追加清理"的写法成为可能(dsh 的 agent factory 就用复合 effect 保证"注册 agent → 宣布创建 → 逆序回收"的原子生命周期)。
2.4 服务解析:provide / get
reflect.ts 第 277 行附近是 provide 的要点:
ts
provide(name: string, value?: any, check?: () => boolean) {
// …登记服务实现…
// 关键:调用方 fiber 被 effect 挂接 —— 提供者卸载,服务自动消失
// ctx.provide(name, impl) 的返回 disposer 同样幂等
}读服务则走 ReflectService.get(name, strict):从当前上下文的作用域链(含 isolate 表)里找实现。第 2 章说的"依赖变化触发级联重载"就发生在这里的上下游:fiber 的 inject 声明让它在服务出现时被唤醒(_refresh),服务消失时被卸载。
2.5 一个完整的读代码练习:追一次 ctx.on 调用
把本节串起来,追一次最普通的调用:
一条链上验证了:上下文代理(2.1)、服务解析(2.4)、事件注册(2.2)、effect 记账(2.3)。这就是 Cordis 的全部魔法——没有运行时黑盒,每一环都是普通函数调用。
2.6 为什么 vendor:读源码前值得知道的事
dsh 没有把 Cordis 当普通 npm 依赖,而是 vendor(内嵌源码)进仓库(vendor/cordis/),并定期与上游 cordiverse/cordis 同步(见 vendor/README.md)。原因在官方文档里说得很直白:harness 插件作者依赖的是这套精确语义(waterfall 的短路约定、effect 的逆序清理),vendor 让 dsh 能锁定并深度定制这些语义,而不被上游发版节奏带着走。代价是仓库多了一份代码,收益是"插件框架"这个地基完全可控。
2.7 本章小结
Cordis 内核的四个事实:Context 是 Proxy,服务是运行时解析的;事件分发的五种模式实现都是几行代码;fiber 是 effect 的账本,卸载按逆序结算;provide/get 构成服务的作用域解析。 下一篇开始拆 dsh 自己的核心包。
小练习:手写一个 waterfall
不看源码,用 10 行以内的 TypeScript 实现一个 waterfall:输入监听器数组和最终 next,返回组合后的函数;监听器可以调 next() 委托,也可以直接返回短路。写完后与 events.ts 第 234–243 行对照。这个练习做完,你对"中间件"的理解就落地了。