🎯前端进阶篇:掌握 JS 异步任务取消

130 阅读3分钟

异步任务取消不是“锦上添花”,而是高并发、重交互应用下的默认生存能力。本文带你从真实场景出发,系统梳理 7 种主流取消方案 + 最佳实践,让你一次搞懂「何时取消、怎么取消、取消到什么程度」。

一、为什么需要取消异步任务

原因说明
资源优化避免无用网络请求 / 计算浪费
状态一致性过期结果不再写入当前状态
用户体验用户改主意、跳转页面时及时止损
错误预防组件已卸载,回调却仍在执行 → 内存泄漏 / 白屏

二、高频真实场景

  1. 组件卸载后仍请求 → 写已销毁的 state
  2. 搜索框连续输入 → 前面联想请求已无意义
  3. 用户狂点按钮 → 只应保留最后一次
  4. 路由跳转 → 旧页面请求全部废弃
  5. 大文件上传超时 → 超过 30 s 自动中断

三、7 种取消实现方式(含代码)

方案适用场景关键 API / 思路
标志位极简 demo、一次性逻辑isCancelled = true
AbortControllerFetch、主流浏览器fetch(url, {signal})
clearTimeout / clearInterval定时器clearTimeout(id)
可取消 Promise 包装任意 Promisereject({isCancelled:true})
RxJS unsubscribe流式场景subscription.unsubscribe()
React useEffect 清理组件生命周期return () => controller.abort()
并行任务统一取消多请求竞速Promise.all + 自定义 cancelToken

下面给出可直接粘贴运行的完整示例

1. 标志位取消法(原理级演示)

let isCancelled = false;

function asyncTask() {
  return new Promise(resolve => {
    setTimeout(() => {
      if (!isCancelled) resolve('任务完成');
      else console.log('任务已取消');
    }, 1000);
  });
}

asyncTask().then(console.log);
setTimeout(() => (isCancelled = true), 500);

缺点:异步逻辑仍在跑,仅结果不生效。

2. AbortController 取消 Fetch(浏览器原生)

const controller = new AbortController();
const { signal } = controller;

fetch('https://api.example.com/data', { signal })
  .then(r => r.json())
  .then(console.log)
  .catch(err => {
    if (err.name === 'AbortError') console.log('请求已取消');
    else console.error('请求出错', err);
  });

setTimeout(() => controller.abort(), 500);

优点:浏览器自动终止 TCP 连接,真正省流量。

3. 定时器取消

const timer = setTimeout(() => console.log('不会执行'), 1000);
clearTimeout(timer);   // 同理 clearInterval

4. 可取消 Promise 包装器(库级别通用)

function makeCancellable(promise) {
  let isCancelled = false;

  const wrapped = new Promise((resolve, reject) => {
    promise.then(
      val => (isCancelled ? reject({ isCancelled, val }) : resolve(val)),
      err => (isCancelled ? reject({ isCancelled, err }) : reject(err))
    );
  });

  return { promise: wrapped, cancel: () => (isCancelled = true) };
}

// 使用
const task = makeCancellable(new Promise(r => setTimeout(() => r('ok'), 1000)));
task.promise.then(console.log).catch(e => e.isCancelled && console.log('取消'));
setTimeout(() => task.cancel(), 500);

5. RxJS 流取消

import { interval } from 'rxjs';

const sub = interval(1000).subscribe(console.log);
setTimeout(() => sub.unsubscribe(), 5000);

6. React 组件内取消(useEffect)

import { useEffect, useState } from 'react';

function DataFetcher({ id }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    (async () => {
      try {
        const res = await fetch(`/api/data/${id}`, { signal: controller.signal });
        setData(await res.json());
      } catch (e) {
        if (e.name !== 'AbortError') console.error(e);
      }
    })();
    return () => controller.abort(); // 组件卸载或 id 变化时取消
  }, [id]);

  return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}

7. 并行任务统一取消(Promise.all 版)

function cancelablePromise(promise, cancelToken) {
  return new Promise((resolve, reject) => {
    cancelToken.promise.then(() => reject(new Error('取消')));
    promise.then(resolve, reject);
  });
}

const cancelTokenSource = {
  promise: new Promise(res => (cancelTokenSource.cancel = res))
};

Promise.all([
  cancelablePromise(fetch('/api/data1'), cancelTokenSource),
  cancelablePromise(fetch('/api/data2'), cancelTokenSource)
])
  .then(console.log)
  .catch(console.warn);

setTimeout(() => cancelTokenSource.cancel(), 1000);

四、优雅最佳实践(生产级 checklist)

  1. 优先原生:Fetch / Axios 新版已支持 signal,直接用
  2. 封装复用:提供统一 useCancellableRequestAsyncCanceller
  3. 错误区分:取消 ≠ 异常;AbortError 单独处理,不弹红屏
  4. 生命周期:组件卸载、路由跳转、页面隐藏时统一取消
  5. 超时兜底Promise.race([fetch, timeout]) 防止永久挂起
  6. 用户反馈:取消后给出 Loading → 已取消 的明确提示
  7. 可观测:打点记录「取消率」,大促期间可省 30%+ 带宽

五、总结

异步取消不是边缘功能,而是性能、稳定性、体验的三重保险:

  • 选对场景 → 用对 API → 封装统一 → 监控可观测
    记住这四步,你的应用就能在流量洪峰下优雅止损,避免“取消忘记打、内存泄漏、数据串台”的经典三连坑。

把上面的 7 段代码全部跑通,你对 JS 异步取消的掌握就超过 90% 开发者了。祝你编码愉快!