原语阶梯的第一级——
Task。它是「一个可取消的异步操作」的标准化封装:带 id、带状态、自带 AbortController、可组合外部信号。这篇拆task.ts的 Task 类(29 篇)和两个 Runner(30 篇)——这是 SPF 处理并发的地基。
Task 解决什么问题
流引擎里到处是异步操作:fetch 一个分段、append 到 SourceBuffer、解析一个 playlist。这些操作有共同需求:
- 可取消——换轨道/销毁引擎时要中止在飞操作。
- 有状态——外面想问「这个操作完成了吗?结果是什么?」
- 有身份——同一操作不该重复发起(按 id 去重)。
裸 Promise 一个都不满足(不可取消、无状态查询、无身份)。Task 把这三样标准化。
TaskLike:最小契约(L44-51)
// task.ts:44-51
export interface TaskLike<TValue, TError> {
readonly id: string;
readonly status: TaskStatus; // 'pending' | 'running' | 'done' | 'error'
readonly value: DeepReadonly<TValue> | undefined;
readonly error: DeepReadonly<TError> | undefined;
run(): Promise<TValue>;
abort(): void;
}
注意 Runner 全部面向这个接口编程,不依赖 Task 具体类(下篇会看到)。你可以用自己的类实现 TaskLike 塞进 Runner。DeepReadonly(L9-13)递归只读映射——value/error 一旦可观察就不该被改。
Task 类的核心实现(L65-113)
字段:不用信号(L66-73)
readonly id: string;
readonly #runFn: (signal: AbortSignal) => Promise<TValue>;
readonly #abortController = new AbortController(); // L68 — 自带
readonly #signal: AbortSignal; // L69 — 组合后的
#status: TaskStatus = 'pending'; // L71 — 普通私有字段
#value: TValue | undefined;
#error: TError | undefined;
值得注意:Task 没用 signal-polyfill 存状态,就是普通私有字段 + getter。状态变化不需要触发响应式更新——Runner 用回调感知完成,不订阅状态。这个克制很对:不是所有状态都得是信号。
构造函数:信号组合(L75-82)
constructor(runFn, config?: TaskConfig) {
this.#runFn = runFn;
const rawId = config?.id;
this.id = typeof rawId === 'function' ? rawId() : (rawId ?? generateId()); // L78
this.#signal = config?.signal
? anyAbortSignal([this.#abortController.signal, config.signal]) // L79-80
: this.#abortController.signal;
}
L79-80 又是 anyAbortSignal(07 篇的 utils 函数)——内部 abort 和外部 signal 任一触发即中止。外部信号让父级取消传播进来(比如整个引擎的销毁信号),内部 controller 保证自己总能独立 abort。id 支持函数形式(构造时调用一次,L78),undefined 时自动生成。
run():状态排序保证(L96-108)
async run(): Promise<TValue> {
this.#status = 'running'; // L97
try {
const result = await this.#runFn(this.#signal);
this.#value = result; // value before status — ordering guarantee
this.#status = 'done'; // L101
return result;
} catch (e) {
this.#error = e as TError; // error before status — ordering guarantee
this.#status = 'error'; // L105
throw e; // L106 — rethrow
}
}
状态排序保证(JSDoc L61-63 原文):value 在 status 变 ‘done’ 之前写入;error 在 status 变 ‘error’ 之前写入。任何观察到 status === 'done' 的读者都保证 value 已就位。这是无锁的单线程版本「happens-before」——JS 单线程下这两个赋值之间不会插入别的代码,但顺序本身是契约:反过来写(先 status 后 value)会让「看到 done 就读 value」的代码读到 undefined。
错误 rethrow(L106)——调用方不管的话会 unhandled rejection,这个防护在 Runner 层(下篇 L145 的 promise.catch(() => {}))。
还有个细节:run() 不检查重复调用——没有 status !== 'pending' 守卫。去重职责在 Runner 层。这个分层很清晰:Task 是「执行单元」,Runner 才是「调度策略」。
abort()(L110-112)
abort(): void {
this.#abortController.abort();
}
只 abort controller——通过组合信号传导给 runFn,期望 runFn 检查信号提前退出。Task 自身不会因此自动把 status 置为 error——状态转移仍由 runFn 抛错驱动(runFn 收到 abort 后应该抛 AbortError,走 L103-106 的 error 分支)。
这个设计初看有点意外,细想很合理:abort 是「请求取消」,怎么响应是 runFn 的事。有的 runFn 可能选择忽略 abort 完成正常工作——Task 忠实记录最终结果。
Task 在流引擎里的用法
拿 41 篇会详讲的场景预览。fetch 一个分段:
const task = new Task(
(signal) => fetch(segment.url, { signal }).then((res) => res.arrayBuffer()),
{ id: `segment:${segment.uri}`, signal: engineDestroySignal }
);
runner.schedule(task); // 交给 Runner 调度
- 引擎销毁 → engineDestroySignal abort → fetch 取消。
- 轨道切换 → runner.abortPending() → 内部 controller abort → 同样取消。
- 重复 schedule 同 id 的任务 → Runner 去重(下篇)。
Task + AbortSignal + Runner 三件套覆盖了流引擎的全部取消场景。
小结
- TaskLike 最小契约——Runner 面向接口编程。
- 自带 AbortController + anyAbortSignal 组合外部信号——父级取消自动传播。
- 状态排序保证——先 value/error 后 status,看到 done 就能安全读 value。
- abort 只发信号不设状态——响应取消是 runFn 的职责。
- 不用信号存状态——状态查询用 getter,完成感知用回调,克制。
- 重复调用防护在 Runner——Task 是执行单元不是调度器。
下一篇讲两个 Runner——SerialRunner 的 promise 链串行化和 ConcurrentRunner 的按 id 去重。
527



被折叠的 条评论
为什么被折叠?



