需从执行模型、状态表示、控制流契约三层面协同设计:定义含id/timestamp/input/output/parent_id/branch_id/is_canceled的不可变状态单元;构建以BranchHandle管理的DAG执行图;封装异步原语为可响应abortSignal的版本;提供分支追溯、时间旅行调试与精准回滚能力。

要手动实现一个支持“状态溯源”与“逻辑分支取消”的复合异步执行引擎,核心在于将异步流程建模为可追溯、可干预的状态机,并在运行时保留足够的上下文以支持回溯与分支级中止。这不是简单封装 Promise 或加个 cancelToken 就能解决的,需要从执行模型、状态表示、控制流契约三个层面协同设计。
一、定义可溯源的状态单元(State Unit)
状态溯源不是记录日志,而是让每个关键执行节点生成不可变、带唯一标识和因果关系的状态快照。
- 每个状态单元包含:id(UUID)、timestamp、input(输入参数快照)、output(输出或异常)、parent_id(前驱状态ID)、branch_id(所属逻辑分支标识)、is_canceled(是否被主动取消)
- 所有状态写入内存/轻量存储(如 Map 或 IndexedDB),按 branch_id + timestamp 索引,便于按分支回溯
- 避免直接存闭包或 this 上下文——改用序列化数据 + 显式依赖注入。例如:不存 function() { return x + y },而是存 { op: 'add', args: ['x', 'y'], env: { x: 5, y: 3 } }
二、构建带分支拓扑的执行图(Execution Graph)
把异步流程抽象为有向无环图(DAG),节点是状态单元,边代表控制流或数据依赖。分支取消的本质,就是对子图的原子性截断与标记。
- 每个异步操作返回一个 BranchHandle 对象,含:
id(分支唯一标识)、cancel()(触发该分支及其所有未完成后代的取消)、waitForCompletion()(返回该分支最终状态 Promise) - 分支创建时自动继承父分支 ID,并生成新 branch_id;并行分支共享同一 parent_id,但拥有不同 branch_id
- 执行器内部维护一个
graph: Map<branchId, { node: StateUnit, children: Set<branchId>, parent: branchId? }>,用于快速定位与裁剪
三、实现可中断的异步调度器(Cancellable Scheduler)
原生 Promise 无法真正取消,因此所有异步原语(fetch、setTimeout、async fn)必须包装为可响应取消信号的版本。
- 每个异步任务启动时绑定当前 branch_id 和一个 abortController(其 signal 注册到 branch 的生命周期管理器)
- 当调用
branch.cancel(),触发:① 向所有活跃子任务 signal.abort();② 将对应状态单元的 is_canceled 设为 true;③ 从 graph 中移除该分支后续待调度节点 - 关键细节:Promise 链中需显式检查 signal.aborted 并提前 resolve/catch,例如:
if (signal.aborted) return Promise.reject(new AbortError())
四、提供状态溯源与分支调试能力
引擎的价值不仅在于执行,更在于可观测性。暴露简洁 API 支持按需还原任意分支的历史路径。
-
engine.getBranchTrace(branchId):返回从根到该分支末端的完整状态单元数组(按时间序),含 input/output 变化链 -
engine.getStateAtTime(branchId, timestamp):二分查找最近状态快照,支持“时间旅行式”调试 -
engine.cancelBranchUpTo(branchId, stateId):取消该分支中指定状态之后的所有后续执行(实现“回滚到某步”语义)
不复杂但容易忽略的是:状态单元必须轻量且可序列化,分支拓扑不能隐式耦合业务逻辑,取消信号必须穿透到底层 I/O 层。真正的难点不在代码量,而在定义清晰的边界契约——比如规定所有用户函数必须接受 { signal, context } 作为第二参数,且禁止在 async 函数外捕获未处理的 rejected promise。做到这些,引擎就既是执行器,也是调试器和状态审计工具。

















