Skip to content
设计可取消函数
概述
Promise 一旦创建就无法从外部中止,已注册的 .then() 回调会持续执行,因此单纯使用 Promise 链难以实现外部取消异步序列的需求。Generator 提供了暂停函数执行的能力,可以把异步流程的控制权交还给调用方,从而在每一步之后检查是否需要终止后续操作。
基本概念
- 可取消操作:在异步序列中,不再执行尚未开始的步骤,但不会中止已经发出的异步任务(例如网络请求)。
- Generator:
function*声明的函数,可以通过yield暂停执行,并由外部通过.next()恢复。yield出 Promise 后,外部调度器可以等待 Promise 落定,再决定是否继续执行下一步。
工作原理
封装函数 cancellable 接收一个 Generator 对象,该 Generator 内部 yield 出 Promise。调度器的工作流程是:调用 generator.next() 获取第一个 yield 出的 Promise,接着 await 该 Promise 直到落定,然后检查取消标志。如果已置为 true,则让外部 Promise 拒绝并终止;如果未取消,则用落定值调用 generator.next() 继续执行下一段,重复上述过程。当 Generator 执行完毕(done 为 true)后,最后一次检查取消标志,若未取消则 resolve 最终值。
这套机制相当于在每个 yield 之后埋入一个取消检查点,取消不是抢占式的,而是协作式的——必须等到当前 Promise 落定后才会响应。
基本用法
cancellable 返回一个元组 [cancel, promise]:
cancel函数:调用后设置内部取消标志。promise:代表整个异步序列的执行结果。若被取消则拒绝并带回'Cancelled';若正常完成则 resolve 最终返回值;若 Generator 内部抛出异常且未被取消,则 reject 原始错误。
API
实现
js
function cancellable(generator) {
let cancelled = false;
const cancel = () => { cancelled = true; };
const promise = (async () => {
let next = generator.next();
while (!next.done) {
try {
const value = await next.value;
if (cancelled) throw 'Cancelled';
next = generator.next(value);
} catch (e) {
// 取消优先生效,即使 Generator 抛出或 Promise reject
if (cancelled) throw 'Cancelled';
throw e;
}
}
// Generator 结束后仍需检查取消标志
if (cancelled) throw 'Cancelled';
return next.value;
})();
return [cancel, promise];
}类型签名
ts
function cancellable<T>(
generator: Generator<Promise<any>, T, unknown>
): [() => void, Promise<T>]generator:一个 Generator 对象,产生Promise<any>类型的值,最终返回类型T。- 返回值:长度为 2 的元组,
cancel为取消函数,promise为执行结果。
示例
正常执行(未取消)
ts
function* steps() {
const a: string = yield new Promise(resolve => setTimeout(() => resolve('step1'), 500));
console.log(a); // step1
const b: string = yield new Promise(resolve => setTimeout(() => resolve('step2'), 500));
console.log(b); // step2
return 'done';
}
const [cancel, promise] = cancellable(steps());
promise.then(
result => console.log('result:', result),
reason => console.log('rejected:', reason)
);
// 输出:
// step1
// step2
// result: done调度器依次等待两个 500ms 的 Promise,未调用 cancel,最终打印 done。
在第一步后取消
ts
function* steps() {
const a: string = yield new Promise(resolve => setTimeout(() => resolve('step1'), 500));
console.log(a);
const b: string = yield new Promise(resolve => setTimeout(() => resolve('step2'), 500));
console.log(b);
return 'done';
}
const [cancel, promise] = cancellable(steps());
// 500ms 后第一个 Promise 落定,紧接着触发下一个 yield 前,取消
setTimeout(cancel, 500);
promise.then(
result => console.log('result:', result),
reason => console.log('rejected:', reason)
);
// 输出:
// step1
// rejected: Cancelled第一个 Promise 落定后 await 完成,打印 step1,然后检查取消标志并发现已取消,最终 promise 拒绝为 'Cancelled',第二个 yield 的 Promise 不会被 await,因此 step2 不会打印。
内部抛出异常且已取消
ts
function* steps() {
const a = yield Promise.resolve('a');
// 模拟错误
throw new Error('some error');
}
const [cancel, promise] = cancellable(steps());
cancel(); // 马上取消
promise.catch(reason => console.log('rejected:', reason));
// 输出:
// rejected: CancelledGenerator 内部抛出 Error,但 catch 分支内发现取消标志已置位,因此原始错误被吞掉,最终以 'Cancelled' 拒绝——调用方已经不再关心正常错误。
注意点
- 不会中止正在进行的 Promise。取消只阻止后续步骤,已经
await的 Promise 会完整地执行到落定。这种行为与AbortController类似——要真正中断网络请求,需要fetch配合signal;封装层仅能丢弃结果,无法中断底层 I/O。 - 检查点必须覆盖。代码中在每个
yield的 Promise 落定后以及 Generator 执行完毕时都做了取消检查,缺少任何一处都可能在取消时仍然继续执行后续步骤或让 promise 进入错误路径。 - 错误处理中的取消优先级。catch 分支需要优先判断取消标志,将取消原因返回给调用方,而不是暴露原始错误。一旦取消,外部已经不再关心业务流程中的异常。
- 类型签名
Generator<Promise<any>, T, unknown>明确了 yield 的是 Promise,最终返回值为T。
限制
- 无法抢占正在执行的代码。如果 Generator 内部存在不产生
yield的耗时同步操作(例如大循环),取消在这个期间完全无效,必须等到下一个yield点才能响应。 - 协作式取消。本例在调度器中直接终止迭代,无需 Generator 内部协作,但调度器本身仍依赖
yield点作为检查时机。 - 已在飞行中的 Promise。取消无法撤回已经发出的异步调用,如果这些调用有副作用(如写入数据库、发送消息),取消后副作用仍然存在。
应用
- 多步骤表单提交流程,允许用户中途放弃。
- 轮询逻辑的优雅终止:每个轮询周期 yield 出一个请求 Promise,外部可随时停止。
- 动画编排序列:分阶段动画可以用 Promise 表达,通过取消跳过剩余阶段。
- 一次发起多个关联请求但允许在结果未完全返回时丢弃后续处理。
