可取消的异步任务与 AbortController

1.AbortController 底层原理

  • AbortController 是Web API标准(whatwg),在浏览器和Node.js 15+都可用。
  • 内部维护一个 AbortSignal 实例,该实例有一个标志 aborted 和一个回调列表(监听abort事件)。
  • 调用 controller.abort() 时,将 signal.aborted 设为 true,并通过触发所有通过 signal.addEventListener("abort") 注册的回调。
  • 支持自定义异步任务取消:在异步操作内监听 signal.abort 事件,主动中断操作(例如清理定时器、拒绝Promise)。

2.广泛使用场景

  • Fetch请求取消:避免过期请求返回后,覆盖最新数据。
  • 事件 监听器 清理:使用 signal 一次性移除多个监听事件。
  • 防抖/节流增强:可取消延迟执行。
  • 异步队列任务:取消未开始的批量任务。
  • React 组件卸载时:取消所有进行中的请求。

3.深度代码示例:

3.1 示例:取消fetch请求

ini 复制代码
const controller = new AbortController();
const { signal } = controller;

fetch('/api/data', { signal })
    .then(res => res.json())
    .catch(err => {
        if(err.name === 'AbortError') console.log('请求已取消');
    })
    
// 延迟100ms后取消
setTimeout(() => controller.abort(), 100);

3.2 示例2:同时取消多个异步任务(使用同一个signal)

javascript 复制代码
function cancellableDelay(ms, signal) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(resolve, ms);
        if(signal) {
            signal.addEventListener('abort', () => {
                clearTimeout(timer);
                reject(new DOMException('Aborted', 'AbortError'));
            })
        }
    });
}

const controller = new AbortController();
cancellableDelay(1000, controller.signal).catch(e => console.log(e.name));
setTimeout(() => controller.abort(), 500); // 500ms后取消

3.3 示例3:封装支持取消的异步迭代器(生成器)

javascript 复制代码
async function* cancellableGenerator(iterable, signal) {
    for(const item of iterable) {
        if(signal.aborted) throw new Error("Generator aborted");
        // 模拟异步处理
        await new Promise(resolve => setTimeout(resolve, 100));
        yield item;
    }
}

const controller = new AbortController();
const gen = cancellableGenerator([1,2,3,4,5], controller.signal);
(async () => {
    for await (const val of gen) {
        console.log(val);
        if(val === 3) {
            controller.abort();
        }
    }
})();

4. 项目实战:实现一个支持取消、超时、重试的通用请求函数

需求:封装 fetchWithRetry,支持取消(AbortController)、超时自动取消、失败重试(指数退避)。

javascript 复制代码
async function fetchWithRetry(url, options = {}) {
    const { timeout = 5000, maxRetries = 3, retryDelay = 1000, ...fetchOptions } = options;
    let lastError;
    const outerController = new AbortController();
    
    // 超时定时器
    const timeoutId = setTimeout(() => outerController.abort(), timeout);
    
    for(let attempt = 0; attempt <= maxRetries; attempt++) {
        const controller = new AbortController();
        const signal = controller.signal;
        // 父级取消时,子请求也取消
        outerController.signal.addEventListener('abort', () => controller.abort());
        
        try {
            const response = await fetch(url, { ...fetchOptions, signal });
            clearTimeout(timeoutId);
            if(!response.ok) throw new Error(`HTTP ${response.status}`);
            return response;
        } catch (err) {
            lastError = err;
            if(err.name === 'AbortError'){
                // 超时或主动取消不再重试
                throw new Error('Request aborted or timeout');
            }
            if (attempt === maxRetries) break;
            // 指数退避延迟
            const delay = retryDelay * Math.pow(2, attempt);      
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
    throw lastError; 
}

// 使用
const controller = new AbortController();
fetchWithRetry('https://api.example.com/data', { timeout: 2000 })
    .then(res => res.json())
    .catch(e => console.error(e));
// 可手动取消
// controller.abort(); 

5. 大厂面试官追问

Q1: AbortController 能否取消正在执行的Promise链?例如已经then注册的回调?

不能直接取消已经开始执行的Promise链(因为Promise一旦创建就无法撤销)。但是可以在每个then回调中检查signal.aborted并提前返回或throw。更优雅的方式是使用Promise.race配合reject信号。

Q2: 如何实现一个可取消的 setTimeout 但不用 clearTimeout,而是用Promise?

利用Promise.race

javascript 复制代码
function cancellableDelay(ms, signal) {
    const delayPromise = new Promise(resolve => setTimeout(resolve, ms));
    const abortPromise = new Promise((_, reject) => {
        if(signal) signal.addEventListener('abort', () => reject(new Error('aborted')));
    });
    return Promise.race([delayPromise, abortPromise]);
}

Q3: AbortSignal 可以实现超时自动触发吗?Node.js 中的 AbortSignal.timeout() 是什么?

可以!Node.js 17.3+ 支持 AbortSignal.timeout(ms),返回一个在指定毫秒后自动 abort 的 signal。浏览器中可使用 AbortSignal.timeout (较新版本)。手动实现:new AbortController() + setTimeout(() => controller.abort(), ms)。

Q4: 如何使用 AbortController 取消一个正在进行的WebSocket连接?

标准的 WebSocket API 没有原生支持 AbortController,但是可以包装:在abort事件中调用 ws.close() 并reject。或者使用ws库(Node.js)支持signal选项。

6.API跨环境适用性总结

API 浏览器 Node.js 通用 实际使用场景
queueMicrotask 安全地将任务放入微任务队列,避免 Promise 开销。适用于库中需要异步但优先级高的操作。
MessageChannel ✅ (需 require('worker_threads').MessageChannel 或全局?Node 16+ 支持 MessageChannel 全局) 部分 跨线程/跨 iframe/跨 Worker 通信。在微前端或复杂 Web Worker 通信中非常有用。
requestIdleCallback ❌ (可用 setImmediate 或第三方 idle-gc) 浏览器中执行低优先级后台任务(如数据预加载、埋点上报),避免影响动画。
requestAnimationFrame 浏览器动画、批量 DOM 更新、帧率控制。
setImmediate ❌ (非标准) Node.js 中在当前事件循环的 check 阶段立即执行,比 setTimeout(fn,0) 快。
process.nextTick Node.js 中在同一阶段内优先执行的任务,用于错误处理或继续执行。
AbortController ✅ (Node 15+) 取消 fetch、自定义异步任务、事件监听器批量移除。

6.1实际应用案例:

  • queueMicrotask:在Vue或者React的nextTick类似实现中,用于在DOM更新后执行回调。
  • MessageChannel:React调度器(Scheduler)中使用MessageChannel产生宏任务,以不延迟的方法实现高优先级更新。
  • requestIdleCallback:Google Analytics在空闲时发送数据;React的fiber并发模式曾经依赖他(现用MessageChannel)。
  • requestAnimationFrame:滚动视差动画、Canvas游戏循环、高性能拖拽。
  • setImmediate(Node.js):在递归调用中替代setTimeout(fn,0)避免延迟钳制。
  • process.nextTick:用于确保事件循环继续之前执行某些操作,例如 EventEmitter 的错误处理。
相关推荐
触底反弹2 小时前
🚀 手把手用 HTML5 Canvas 从零打造飞机大战游戏,代码全开源!
前端·javascript·canvas
DJ斯特拉2 小时前
axios快速使用
开发语言·前端·javascript
Hilaku3 小时前
AI 写代码越快,为什么 Code Review 越不能省?
前端·javascript·程序员
HjhIron4 小时前
CSS 3D 世界:从盒子模型到三维空间动画
javascript·css
VidDown4 小时前
显卡处理视频技术详解:从硬解码到 NVENC,GPU 如何让视频处理起飞?
javascript·编辑器·音视频·视频编解码·视频
代码不加糖5 小时前
Proxy能够监听到对象中的对象的引用吗?
开发语言·前端·javascript
大家的林语冰5 小时前
连 Markdown 都不放过,Rust 在前端基建杀疯了,万物皆可“锈化“!
前端·javascript·markdown
想吃火锅10055 小时前
【前端手撕】instanceof
前端·javascript·原型模式
один but you5 小时前
const和constexpr常量表达式
java·前端·javascript