任务队列的中断和恢复

2025-07-02 前端

在 Web 前端开发中,我们经常需要处理一系列异步任务:批量上传图片、逐条导入数据、分片处理大文件、后台轮询接口……这些场景都涉及任务队列的管理。而其中最难处理的问题之一就是:如何优雅地中断一个正在执行的任务队列,并在合适的时机恢复它?

一、为什么需要中断和恢复?

先看几个真实场景:

场景 中断原因 恢复条件
批量图片上传 用户点击"取消" 重新点击"开始"
数据导入 页面切换/网络断开 回到页面/网络恢复
后台计算 CPU 占用过高 用户主动要求继续
轮询请求 组件卸载 组件重新挂载

如果没有中断和恢复机制,只能粗暴地 clearTimeout / ignore promise,但这样会丢失进度和中间状态。

二、基础版:使用标志位控制

最简单的中断方式是用一个布尔标志位。

使用示例:

const queue = new TaskQueue();

queue.add(() => fetch('/api/step/1'));
queue.add(() => fetch('/api/step/2'));
queue.add(() => fetch('/api/step/3'));

queue.run();

// 5 秒后暂停
setTimeout(() => queue.pause(), 5000);
// 10 秒后继续
setTimeout(() => queue.resume(), 10000);

优点:实现简单,零依赖。
缺点:无法取消正在执行中的单个任务,退出时需要等待当前 task 完成。

三、升级版:用 AbortController 实现取消

AbortController 是现代 JavaScript 中标准的中断方案,可以让正在进行的异步操作响应取消信号。

配合支持 signal 的 API 使用效果最佳:

const queue = new AbortableTaskQueue();

queue.add(async (signal) => {
  const resp = await fetch('/api/big-file', { signal });
  return resp.json();
});

queue.add(async (signal) => {
  // 手动检查中断信号
  for (let i = 0; i < 1000; i++) {
    if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
    await processChunk(i);
  }
});

queue.run();

// 随时取消
setTimeout(() => queue.cancel(), 3000);

fetch、axios、fs.readFile(Deno/Node)等主流异步 API 都原生支持 AbortSignal。

四、带进度的可恢复队列

中断后恢复的核心难点是保存进度。我们需要记录每个任务的状态和已完成的部分。

使用示例——分片上传场景:

五、用 Generator 实现优雅的中断点

Generator 函数天然支持"暂停-恢复",非常适合任务队列场景。

function* taskSequence() {
  yield* processFiles();
  yield* validateData();
  yield* uploadResults();
}

function* processFiles() {
  for (const file of files) {
    // 处理每个文件后都可以中断
    yield { type: 'progress', file: file.name };
    const data = readFile(file);
    yield { type: 'processed', file: file.name, data };
  }
}

配合一个通用的执行器:

这种模式的好处是中断点是显式的——你在 yield 处自然暂停,任务状态完全由 Generator 的调用栈保存,恢复时从 yield 后面继续执行。

六、在 Web Worker 中中断和恢复

对于 CPU 密集型任务,Web Worker 是必经之路。中断和恢复通过 postMessage 传递控制信号。

worker.js:

主线程:

const worker = new Worker('worker.js');

worker.postMessage({
  type: 'start',
  tasks: [data1, data2, data3],
});

// 暂停
worker.postMessage({ type: 'pause' });

// 恢复
worker.postMessage({ type: 'resume' });

// 取消
worker.postMessage({ type: 'cancel' });

这种方式的好处是 Worker 线程的计算完全不影响主线程响应,用户交互不会被阻塞。

七、React 中的任务队列恢复

React 组件卸载时自动中断,重新挂载时恢复,是 Hooks 的经典用例。

function useTaskQueue() {
  const queueRef = useRef(new ResumableTaskQueue());

  useEffect(() => {
    return () => {
      queueRef.current.pause();
    };
  }, []);

  const run = useCallback(async (tasks) => {
    tasks.forEach((t, i) => queueRef.current.add(i, t));
    await queueRef.current.run();
  }, []);

  return {
    run,
    pause: () => queueRef.current.pause(),
    resume: () => queueRef.current.resume(),
    cancel: () => queueRef.current.cancel(),
    progress: () => queueRef.current.getProgress(),
  };
}

使用:

function UploadPage() {
  const { run, pause, resume, progress } = useTaskQueue();
  const [pct, setPct] = useState(0);

  const handleUpload = () => {
    run(files.map(f => async (signal) => {
      await uploadFile(f, signal);
      setPct(progress().percent);
    }));
  };

  return (
    <div>
      <progress value={pct} max={100} />
      <button onClick={pause}>暂停</button>
      <button onClick={resume}>继续</button>
    </div>
  );
}

八、总结

方案 适用场景 中断粒度 可恢复性
标志位控制 简单队列 任务级 手动实现
AbortController fetch / 回调式异步 任意点 需配合进度保存
Generator 纯计算 / 遍历 yield 处 天然恢复
Web Worker CPU 密集型 循环检查点 消息控制
React Hooks 组件生命周期 自动卸载中断 配合 ref

核心设计原则:

  1. 中断点要显式——不要在深层回调里偷偷中断,容易遗漏清理
  2. 保存足够状态——恢复时最少能回到上一个已完成的任务
  3. 区分暂停和取消——暂停可恢复,取消要释放资源
  4. AbortSignal 是第一选择——它是 Web 标准,原生 API 广泛支持

掌握这些模式后,再也不怕长任务阻塞页面了。无论是批量上传、数据导入还是复杂计算,都能做到想停就停,续上就走。