Cordis — 效应与协同效应机制#
一句话摘要:本章深入讲解 Cordis 如何把论文的「可逆效应」与「响应式协同效应」落地为源码机制——
Fiber.effect返回 disposable(逆)、@Inject/provide实现依赖注入(coeffect)、symbols符号体系、以及getTraceable/createCallable等可追踪代理。
1. 可逆效应:ctx.effect() 与 Disposable#
Cordis 的「可逆效应」抽象集中体现在 ctx.effect()(定义于 packages/core/src/fiber.ts,再经 ReflectService.mixin('fiber', [...]) 暴露到上下文上)。
1.1 Effect 的几种形态#
export type Disposable<T = any> = () => T
export type Effect<T = any> =
| SyncEffect<T> // 返回 disposable 或 disposable 的(同步)可迭代
| AsyncEffect<T> // 返回 Promise<disposable> 或 disposable 的异步可迭代
type SyncEffect<T = any> =
| Disposable<T>
| Iterable<Disposable<T>, void, void>
type AsyncEffect<T = any> =
| Promise<Disposable<T>>
| AsyncIterable<Disposable<T>, void, void>
一个 effect 的返回值可以是一次性的 dispose 函数(Disposable),也可以是多个 dispose 函数的(异步)迭代序列——这正是论文「扭转组合幺半群」的工程形态:一个插件的多次效应按顺序累积,卸载时逆序回收。
1.2 effect 的实现#
Fiber.effect 的核心流程(简写):
effect(execute: () => Effect, label = 'anonymous') {
this.assertActive()
const disposables: Disposable[] = []
const dispose = () => {
// 逆序执行所有 collect 到的 disposable
let task
for (const dispose of disposables.splice(0).reverse()) { /* 顺序 await */ }
return task
}
// runners:execute 的结果被收集进 disposables
const wrapper = defineProperty(() => { runner.epoch = false; return task ? task.then(dispose) : dispose() }, symbols.effect, meta)
disposables.push(this._disposables.push(wrapper))
return wrapper
}
要点:
execute返回的每个 dispose 函数都被「收集(collect)」,并在dispose()中逆序执行——先注册的后清理,这是副作用回退的正确顺序。wrapper 函数被标记了
symbols.effect元信息(含label与子效应树),用于getEffects()观测。每个 wrapper 同时被推进其所属
Fiber的_disposables列表,构成可遍历的效应树。
与论文对应:
execute就是正向变换f,它返回的dispose就是逆g;_disposables的逆序回收对应「扭曲组合」中逆以相反顺序累积((f1,g1)∘(f2,g2) = (f1∘f2, g2∘g1))。
1.3 典型用法#
// 注册一个定时器,并返回清理函数(逆)
const dispose = ctx.effect(() => {
const timer = setInterval(callback, 1000)
return () => clearInterval(timer) // ← 逆函数
}, 'ctx.interval()')
dispose() // 手动撤销(执行逆)
2. 响应式协同效应:注入与提供#
「coeffect」即「组件对环境的依赖」。在 Cordis 中,这一概念由 inject(声明依赖)与 provide(供应依赖)共同实现,并由 ReflectService.notify 驱动响应式更新。
2.1 声明依赖:Inject#
@Inject 装饰器(packages/core/src/registry.ts)可以标注在类或类方法上:
export function Inject<K extends InjectKey>(name: K, config?) {
return function (value: any, decorator: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) {
if (decorator.kind === 'class') {
// 写入 value.inject[name] = config
} else if (decorator.kind === 'method') {
// 记录到 metadata.inject,并注册 init hook
}
}
}
类级 @Inject('logger') 表示「此类需要一个名字为 logger 的服务」。装配时,RegistryService.plugin 调用 Inject.resolve(plugin.inject) 把依赖表归一化为 Dict<string | config>。
2.2 供应依赖:ReflectService.provide#
provide 把一个服务实现注册进 store,并返回「撤销供应」的逆函数(见第 3 章第 5 节)。服务按 isolate symbol 隔离(见第 7 章),因此同一个服务名在不同隔离域可以有不同的实现。
2.3 响应式通知:notify#
当服务被 provide 或 set 变动时,notify 遍历所有插件的所有 fiber,判断其 inject 是否包含被变动的服务,据此更新依赖的可用性:
notify(names, filter) {
for (const runtime of this.ctx.registry.values()) {
for (const fiber of runtime.fibers) {
for (const name of names) {
if (!(name in fiber.inject)) continue
fiber._checkImpl(name)
}
fiber._refresh() // 触发激活/停用
}
}
}
与论文对应:
fiber.inject就是「协同效应规范」;notify就是「上下文变化通知」;_checkImpl/_refresh的结果对应「激活/停用/中性」三种 response。
3. 符号体系(symbols)#
Cordis 大量使用 Symbol.for(...) 定义内部符号(packages/core/src/utils.ts),以规避命名冲突并实现跨模块协作:
export const symbols = {
// 内部符号
shadow: Symbol.for('cordis.shadow'),
caller: Symbol.for('cordis.caller'),
receiver: Symbol.for('cordis.receiver'),
metadata: Symbol.for('cordis.metadata'),
initHooks: Symbol.for('cordis.initHooks'),
// 上下文符号
effect: Symbol.for('cordis.effect'),
filter: Symbol.for('cordis.filter'),
isolate: Symbol.for('cordis.isolate'),
intercept: Symbol.for('cordis.intercept'),
// 服务符号
init: Symbol.for('cordis.init'),
check: Symbol.for('cordis.check'),
config: Symbol.for('cordis.config'),
invoke: Symbol.for('cordis.invoke'),
// ...
}
符号 |
用途 |
|---|---|
|
服务隔离键映射( |
|
服务配置拦截映射( |
|
标记一个函数为 effect, |
|
事件/服务的作用域过滤(coeffect 的观测等价载体) |
|
上下文派生时保留的原型信息,用于 |
|
标记服务「可调用」, |
|
追踪元数据,供 |
4. 可追踪代理:getTraceable 与 createCallable#
Cordis 用 Proxy 实现了一个「可追踪(traceable)」机制,让从服务获取的值自动绑定到正确的上下文。
4.1 getTraceable#
export function getTraceable<T>(ctx: Context, value: T): T {
if (!isObject(value)) return value
if (Object.hasOwn(value, symbols.shadow)) return Object.getPrototypeOf(value)
const tracker = value[symbols.tracker]
if (!tracker) return value
return createTraceable(ctx, value, tracker)
}
若一个值带有 symbols.tracker 元数据,就为其生成一个带上下文感知的 Proxy,使访问它的属性(如调用 service.method())时,this 能正确指向调用方上下文。
4.2 createCallable#
export function createCallable(name: string, proto: {}, tracker: Tracker) {
const self = function (...args: any[]) {
const proxy = createTraceable(self['ctx'], self, tracker)
return Reflect.apply(proxy, this, args)
}
defineProperty(self, 'name', name)
return Object.setPrototypeOf(self, proto)
}
LoggerService 就是通过 createCallable 变成「既可当服务引用、又能像函数一样调用」的对象。
5. DisposableList:逆序回收的数据结构#
DisposableList(packages/core/src/utils.ts)是效应回收的底层结构:
export class DisposableList<T extends WeakKey> {
private map = new Map<number, T>()
push(value: T) { /* 返回 () => this.map.delete(sn) */ }
clear() {
const values = [...this.map.values()]
this.map.clear()
return values.reverse() // ← 逆序返回
}
}
clear() 逆序返回所有元素,配合 Fiber.effect 中的逆序执行,实现了「后注册先清理」的副作用回退顺序。
6. 效应 / 协同效应在实例中的体现#
以 TimerService.timeout(packages/timer/src/index.ts)为例,它同时兼具两种机制:
timeout(callback, delay) {
const dispose = this.ctx.effect(() => {
const timer = setTimeout(() => { dispose(); callback() }, delay)
return () => clearTimeout(timer) // 可逆效应:提供逆
}, 'ctx.timeout()')
return dispose
}
再以 Service 的依赖注入(service.ts 构造函数)为例:
self.ctx.reflect.provide(name, self, this[symbols.check]) // 协同效应:供应服务
return self
二者共享同一个 Context 与 Fiber——这正是论文「统一上下文类型」的实现。
7. 小结表#
概念 |
论文术语 |
Cordis 实现 |
|---|---|---|
副作用 + 撤销 |
可逆效应(revertible effect) |
|
逆序回退 |
扭曲组合(twisted composition) |
|
依赖声明 |
协同效应规范(coeffect spec) |
|
依赖供应 |
上下文变换 |
|
变化通知 |
响应式协同效应(reactive coeffect) |
|
依赖可用性判定 |
激活/停用/中性 |
Fiber 的 epoch 重算与状态切换 |
统一上下文 |
统一上下类型(unified context type) |
|