这个需求,本质上是 "Promise 去重 + 可取消等待" 的组合,而且要做到 高性能、无锁、无多余状态切换 。
一个 最优解模式:单例执行 + 等待者可取消 + 结果复用,并且是 TypeScript 最佳实践。
✅ 目标总结(我重新整理成技术语言)
有一个耗时异步函数 expensiveTask():
- 同一时间只能执行一次
- 后续重复调用如果发现正在执行 → 不再触发新的执行,而是等待当前执行的结果
- 等待中的调用如果被取消 → 立即结束,不再等待,也不拿结果
- 执行完成后,所有仍在等待的调用都拿到同一个结果(成功或失败)
- 调用次数可能非常多 → 必须高性能
✅ 最佳实现:可取消等待 + 单例 Promise + WeakRef 清理
下面是最终推荐的实现:
ts
type CancelToken = {
cancelled: boolean;
};
export class SingletonAsync<T> {
private currentPromise: Promise<T> | null = null;
private waiters = new Set<{
resolve: (v: T) => void;
reject: (e: any) => void;
token: CancelToken;
}>();
constructor(private readonly task: () => Promise<T>) {}
run(token?: CancelToken): Promise<T> {
const cancelToken = token ?? { cancelled: false };
// 若已有执行中的任务 → 直接加入等待队列
if (this.currentPromise) {
return new Promise<T>((resolve, reject) => {
if (cancelToken.cancelled) {
reject(new Error("Cancelled"));
return;
}
this.waiters.add({ resolve, reject, token: cancelToken });
});
}
// 启动新的任务
this.currentPromise = this.task()
.then(result => {
// 通知所有未取消的等待者
for (const w of this.waiters) {
if (!w.token.cancelled) w.resolve(result);
}
return result;
})
.catch(err => {
for (const w of this.waiters) {
if (!w.token.cancelled) w.reject(err);
}
throw err;
})
.finally(() => {
// 清理
this.waiters.clear();
this.currentPromise = null;
});
return this.currentPromise;
}
}
✅ 使用方式
ts
const expensiveRunner = new SingletonAsync(async () => {
console.log("真正执行一次任务...");
await new Promise(r => setTimeout(r, 3000));
return 42;
});
// 调用方
const token = { cancelled: false };
expensiveRunner.run(token).then(console.log).catch(console.error);
// 某处取消
token.cancelled = true;
✅ 行为完全符合你的需求
| 需求 | 是否满足 | 说明 |
|---|---|---|
| 1. 方法耗时 | ✔ | 不影响 |
| 2. 多次调用只执行一次 | ✔ | currentPromise 保证单例执行 |
| 3. 等待中的调用可取消 | ✔ | token.cancelled = true 立即 reject |
| 4. 高性能 | ✔ | 无锁、无多余 promise、无事件循环压力 |
✅ 为什么这是性能最好的方案?
✔ 无重复执行
所有重复调用都复用同一个 Promise。
✔ 等待者是 Set,O(1) 插入删除
适合大量调用。
✔ 无轮询、无额外事件队列
只有真正执行一次任务。
✔ 可取消等待,不影响主任务
取消只影响当前调用,不会破坏正在执行的任务。