系统设计 / 任务运行时 · 从一次 run() 到可观测的调度层 待审核 10 页

任务运行时 · 从一次 run() 到可观测的调度层

一个 fetch 加一个 await 就是一个异步任务,前端应用里同时在飞的往往有几十个:搜索框每敲一下、列表每滚一屏、表单每改一格都在派任务。难点不在发起,在于发起之后的一切——谁先跑、最多几个同时跑、旧的要不要取消、同一个请求连发五次算几次、失败了重试几回、组件卸载了还在飞的怎么办。这些问题各自都有现成答案,凑在一起却互相牵制:取消要能穿过重试与去重,优先级要能穿过依赖与延迟。

本系列的对象是 @vega/job,一台把这些问题放进同一份状态机里处理的任务运行时。每一页对着它的一个切面:任务的九个状态与协作式取消、六档优先级的队列、after 依赖与 flow / parallel / pipeline 编排、debouncethrottle 的时间窗口、同 key 的三种协调策略、重试与超时、作用域与 effects、事件流与 devtools,最后是建在引擎之上的响应式依赖图。

所有 lab 都在浏览器里驱动真实的运行时,不做假的动画:图上每一根条都对应一条 task:startedtask:done 的事件,时间是真实流逝的毫秒。这样做的代价是同一次演示两次的毫秒数不完全相同,正文里凡是引用数字,引的都是与时序无关的量——完成次序、放行数、取消原因、重试次数——并锁进了 core/ 的测试。

任务与队列:一次 run() 会经历什么

先把最小单元看清:一个 TaskFn 从 run() 到终态的九个状态,取消为什么只能是协作式的,父任务倒下时子任务怎样被级联。再看队列:并发上限、六档优先级、暂停与清空,以及「同一 tick 内提交」与「跨 await 提交」的差别。

Task 生命周期

任务与状态机:从 run() 到终态

一次 run() 产生一个 Task 句柄,taskFn 沿九个状态走到三种终态之一。取消是协作式的,cancel() 只翻转 ctx.signal;父任务被取消或出错时,后代一律以 parent 原因级联取消。

队列与并发

队列与并发:优先级与队列开关

默认并发 3,六档 TaskPriority 决定出队次序,同档 FIFO。暂停只挡新任务启动;队列级操作收走的任务以 queue-clear 收尾,与单个 cancelabort 可分辨。

真实运行时与可复现数字

本系列的 lab 全部驱动真实的 @vega/job 运行时,不用固定帧的动画代替。好处是图上看到的每一次排队、抢占与取消都是引擎真的做了的事;代价是毫秒数不可复现——同一个场景两次演示的 Gantt 会差几毫秒。 因此正文引用的数字只取与真实时间无关的量:完成次序、放行个数、取消原因、重试次数、事件条数。这些量在 core/scenarios.test.ts 里用同一份场景跑出来锁死,引擎或场景一改,测试先红。 凡涉及耗时的结论(pipeline 比 barrier 早多少交付首个结果),正文给的是按阶段时长算出的解析值,测试断言的是不等式而不是具体毫秒。 编排与时间:多个任务如何组合

after 把任务连成图,失败沿边传播;flow / parallel / pipeline 把图写成程序,无屏障的 pipeline 比逐阶段的 barrier 早交付首个结果。时间窗口 shaper 把高频调用压成少数几次执行,同 key 协调器决定重复调用是共享、重启还是排队。

任务编排

依赖与编排:从 after 到 pipeline

after 把任务连成依赖图,任一依赖失败即沿边取消下游;flow / parallel / any 把图写成程序;pipeline 让每个 item 独立流过全部阶段,比逐阶段 barrier 更早交付首个结果。

窗口内的合并

时间窗口:debounce 与 throttle

高频调用进同一个时间窗口后被压成少数几次执行。debounce 的窗口随新调用延长,throttle 的窗口固定长度,两者共用一台状态机;被合并掉的调用以 CancelledError 收场,一次都没进过运行时的队列。

coordinator

同 key 协调:重复调用的去向

同一个 key 上连发的调用由 coordinator 裁决:合并成一次执行、后来者取消先到者、排成 FIFO,或原地丢弃。协调是插件化的协议,写一个新策略只有几行。

相关链接

韧性、生命周期与观测

失败之后是重试、超时与降级三件事的组合。作用域把一组任务的生命期绑到组件上,effects 把「伴随任务成对发生的副作用」抽成协议。最后是观测:事件流的两个类别、inspect() 快照、devtools 面板,以及建在引擎之上的响应式依赖图。

韧性 middleware

失败之后:重试与降级

错误先分成可重试与结构性两类,三个 middleware 的行为都由此决定:retry() 只吃可重试的那类,withTimeout 到时 abort 并等 taskFn 退出,withFallback 默认不接超时。

作用域与 effects

作用域与 effects:一组任务的生命期

作用域把一组任务挂在同一个 sentinel 任务下,关掉作用域即级联取消全部后代。effect 协议把伴随任务成对发生的副作用从 taskFn 体里抽出来,setup 的返回值决定 cleanup 何时登记。

事件流 · devtools

观测与调试:事件流与快照

运行时投出的事件是 type / at / payload 三段式,inspect() 只回答此刻。收尾时 whenIdle() 只观察,dispose() 取消一切并发一条 runtime:disposed

reactive graph

响应式依赖图:push 标记与 pull 求值

createReactiveGraph 在引擎之上叠一层记忆与失效:set 沿反向边标记,get 懒重算并按值相等剪枝。异步之下另有 stale guard、subtree cancel 与协调窗口三道机制。

Vue 适配层

Vue 集成:composables 与卸载

@vega/job/vue 把 Task 的状态投影成 ref,把 scope 与 flow 的生命期钉在组件实例上。组件卸载走的是 scope.cancel(),在飞的任务以 parent 原因终止。