promise.all/race无法满足优先级与回收需求,因其不支持任务优先级调度、无法中止已启动任务,且不自动释放内存引用,易致隐式泄漏;priorityexecutor通过最小堆管理优先级、weakmap跟踪元信息实现可控调度与自动回收。

为什么直接用 Promise.all 或 Promise.race 无法满足优先级 + 回收需求
因为它们只管“并发执行完就结束”,既不支持任务插入时指定优先级,也不感知任务中途是否被取消或超时。一旦 Promise 开始执行,就无法中止(除非内部主动抛错或 reject),更不会自动释放内存引用——尤其当任务携带大对象、定时器、事件监听器时,容易造成隐式内存泄漏。
真实场景比如:前端页面有多个数据请求排队,用户快速切换 tab,前一个 tab 的请求应立即降权或终止;又或者后台任务调度中,高优任务插入后,低优任务应被暂停或丢弃。
- 优先级不是靠
setTimeout(fn, 0)模拟的,那只是微任务顺序扰动,不构成可管理的队列结构 - “自动回收”不是调用
delete就行,关键是切断对 Promise 内部 resolve/reject 函数、闭包变量、定时器 ID 的持有 - 不能依赖全局
AbortController简单包装——它只解决 signal 传递,不解决任务在队列中“被跳过”或“被挤出”的逻辑
PriorityExecutor 的核心设计:用堆维护优先级,用 WeakMap 跟踪可回收引用
优先级队列部分用最小堆(小顶堆)实现,但注意:堆顶是**最高优先级**(数值越小,优先级越高),所以比较函数是 (a, b) => a.priority - b.priority;同时每个任务需带唯一 id,用于后续取消。
自动回收的关键在于:不把任务函数、参数、resolve/reject 句柄长期强引用。用 WeakMap 存储任务元信息(如 timer、abortController、状态),一旦任务完成/取消,外部无引用时自动被 GC。
class PriorityExecutor {
#queue = []; // 堆数组
#running = false;
#taskMeta = new WeakMap(); // key: taskFn, value: { controller, timer, status }
<p>enqueue(taskFn, options = {}) {
const id = Symbol('task');
const priority = options.priority ?? 0;
const timeout = options.timeout;
const controller = new AbortController();</p><pre class="brush:php;toolbar:false;">const wrapped = () => {
if (controller.signal.aborted) return Promise.reject(new Error('aborted'));
return Promise.resolve().then(() => taskFn()).catch(err => {
if (!controller.signal.aborted) throw err;
return Promise.reject(err);
});
};
const item = { id, priority, fn: wrapped, controller, timeout };
this.#heapPush(item);
this.#taskMeta.set(wrapped, { controller, timeout });
if (!this.#running) this.#run();
return { cancel: () => controller.abort() };}
heapPush(item) {
this.#queue.push(item);
let i = this.#queue.length - 1;
while (i > 0) {
const p = Math.floor((i - 1) / 2);
if (this.#queue[p].priority <p>}</p><h1>run() {</h1><pre class="brush:php;toolbar:false;">if (this.#queue.length === 0) return;
this.#running = true;
const item = this.#heapPop();
const { fn, controller, timeout } = item;
const timeoutId = timeout && setTimeout(() => controller.abort(), timeout);
Promise.resolve().then(() => fn()).finally(() => {
clearTimeout(timeoutId);
this.#taskMeta.delete(fn);
this.#running = false;
this.#run(); // 继续下一个
});}
heapPop() {
const first = this.#queue[0];
const last = this.#queue.pop();
if (this.#queue.length === 0) return first;
this.#queue[0] = last;
let i = 0;
while (true) {
const l = i * 2 + 1;
const r = i * 2 + 2;
let smallest = i;
if (l <p>}
}</p>常见错误:误以为 AbortController 能自动清理 Promise 闭包
很多人写 const controller = new AbortController(); fetch(url, { signal: controller.signal }) 就以为万事大吉——但 fetch 返回的 Promise 本身仍持有对 URL、headers、甚至 response body 的引用;如果没显式调用 .catch 或 .finally 清理流、释放 blob,这些资源不会被回收。
- 不要只 abort signal,还要在
finally中手动response.body?.cancel()(如果是流式响应) - 避免在 taskFn 里定义长生命周期闭包,比如把整个组件实例传进去;改用 id 查表或事件总线通信
-
WeakMap只能以对象为 key,所以必须把包装后的wrapped函数作为 key,不能用原始taskFn—— 否则每次调用enqueue都新建函数,旧 meta 永远无法被清除
如何安全地“挤出”低优先级任务而不引发竞态
真正难的不是插入高优任务,而是当新高优任务到来时,正在运行或排队中的低优任务要不要 kill。答案是:只 kill 排队中(未开始执行)的任务;对已运行的,最多加个 signal.aborted 检查点,由业务自己决定是否提前退出。
强行中断正在执行的 Promise 是不可能的(JS 无抢占式调度),所以 PriorityExecutor 不提供“强制终止运行中任务”的 API。你看到的 cancel 方法,本质只是触发 abort signal,后续逻辑是否响应,取决于 taskFn 内部是否检查 signal.aborted。
- 想让任务可中断,必须在耗时操作中插检查点:比如循环里每 100 次
if (signal.aborted) return,或 fetch 后立刻if (signal.aborted) throw - 不要在
then链里做 cleanup,因为链可能被多次then,导致重复执行;统一放在finally或 executor 外层包装中 - 如果任务启动了
setInterval,记得在controller.signal.addEventListener('abort', ...)里清除,而不是只靠clearTimeout
自动回收真正起效的前提,是你没在别处偷偷保留对 taskFn 或其闭包的强引用——比如把它塞进某个全局 map、绑定到 DOM 元素的 dataset、或作为 React 组件 state 的一部分。一旦有任意一条强引用链存在,WeakMap 的键就永远不会被 GC,回收也就失效了。










