JavaScript AbortSignal.any实战:一键取消多个fetch和超时,告别手动组合

2026-09-04 0 1,017

做后台管理系统的时候,最怕用户点了“列表刷新”又立刻点了别的菜单。结果先前发的请求慢三分,等你回来的时候数据已经错位了。以前解决这个问题,要么用一个全局的AbortController不停abort,要么给每个请求外面包一层带超时标志的Promise。都不太优雅。

最近接触了 AbortSignal.any()AbortSignal.timeout(),发现这两个API组合起来是真的好使。今天用实际例子把它们讲透。

先说老办法烦在哪儿

假设页面有两个请求,一个用来加载用户列表,一个用来加载菜单。当用户的点击行为导致你希望同时取消这两个请求时,你得自己维护一个数组:

const controllers = [];
function request(url) {
    let controller = new AbortController();
    controllers.push(controller);
    return fetch(url, { signal: controller.signal });
}
// 取消所有
function cancelAll() {
    controllers.forEach(c => c.abort());
    controllers.length = 0;
}

这样暂时能跑,但代码不灵活。比如其中某个请求还要和内部一个“30秒超时信号”组合,那你就得自己写一个复杂工具函数。而且如果请求已经完成了,controller还留在数组里造成内存泄漏。

认识一下AbortSignal.timeout()

AbortSignal.timeout(ms) 是一个直接返回“会在指定毫秒后自动触发abort”的信号。不需要你自己 setInterval 手动调用 abort。

const signal = AbortSignal.timeout(5000);
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
    .then(res => res.json())
    .catch(err => {
        if (err.name === 'TimeoutError') {
            console.log('请求超时了');
        } else {
            console.log('请求失败', err);
        }
    });

和手动版对比,少写了一堆定时器释放代码。而且timeout信号被触发后,自动进入aborted状态,之后如果给另一个请求也用这个signal,它也会立刻中止。

AbortSignal.any():把多个信号捏成一个

关键角色来了。AbortSignal.any([signal1, signal2, ...]) 会返回一个新的信号。这个新信号只要任何一个传入的信号abort了,它也跟着abort。说人话:你拿着这个组合信号去请求,任何一个来源说“别干了”,请求就停。

应用场景:列表请求既想要“手动点取消”按钮去中止,又想要5秒超时自动派单。以前你得把两个条件手动判断:

let controller = new AbortController();
let timeoutId = setTimeout(() => controller.abort(new DOMException('超过时间', 'TimeoutError')), 5000);
document.querySelector('#cancelBtn').onclick = () => controller.abort();

fetch(url, { signal: controller.signal })
    .finally(() => clearTimeout(timeoutId));

现在写成这样就好:

const cancelSignal = new AbortController().signal; // 之后通过外部controlling.abort()触发
const timeoutSignal = AbortSignal.timeout(5000);
const comboSignal = AbortSignal.any([cancelSignal, timeoutSignal]);

fetch(url, { signal: comboSignal })
    .then(res => res.json())
    .catch(err => {
        if (err.name === 'AbortError') {
            // 这里可能是取消按钮触发,也可能是超时触发
            // 想区分具体来源可以检查 timeoutSignal.aborted
            if (timeoutSignal.aborted) {
                console.log('超时导致');
            } else {
                console.log('手动取消导致');
            }
        }
    });

注意,我用 new AbortController().signal 创建了一个永远不会被abort的信号? 上面这个例子为了演示,cancelSignal实际上没有对应的controller可以abort。这里应该写成:

const cancelController = new AbortController();
const cancelSignal = cancelController.signal;
const timeoutSignal = AbortSignal.timeout(5000);
const combined = AbortSignal.any([cancelSignal, timeoutSignal]);

fetch(url, { signal: combined });

// 用户点击取消时
cancelController.abort();

完整案例:一个带手动取消和自动超时的商品列表

我实现了一个常见的后台列表逻辑:进入页面后加载商品数据。加载过程中,用户点“停止加载”按钮会中断列表请求;同时5秒内未加载完成也会自动终止。为了模拟慢网络,我用一个延迟2000ms的mock接口。

<button id="cancelBtn">停止加载</button>
<div id="status">请求未发送</div>
<ul id="list"></ul>

<script>
const cancelBtn = document.getElementById('cancelBtn');
const statusEl = document.getElementById('status');
const listEl = document.getElementById('list');

function fetchProducts() {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            resolve([
                { id: 1, name: '机械键盘' },
                { id: 2, name: '降噪耳机' },
                { id: 3, name: '便携屏' }
            ]);
        }, 3000); // 3秒才返回,模拟慢网络
        // 但我们要加上一个可在1秒后手动取消
        // 这样既能演示取消,又能演示超时
    });
}

// 我们改造fetchProducts,让它支持signal
function fetchProductsWithSignal(signal) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            resolve([
                { id: 1, name: '机械键盘' },
                { id: 2, name: '降噪耳机' },
                { id: 3, name: '便携屏' }
            ]);
        }, 3000);

        // signal中止时清除定时器并reject
        signal.addEventListener('abort', () => {
            clearTimeout(timer);
            reject(new DOMException('操作被中止', 'AbortError'));
        }, { once: true });
    });
}

const cancelController = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
const combinedSignal = AbortSignal.any([cancelController.signal, timeoutSignal]);

cancelBtn.addEventListener('click', () => {
    cancelController.abort();
    statusEl.textContent = '你手动中止了请求';
});

statusEl.textContent = '请求发送中';
listEl.innerHTML = '';

fetchProductsWithSignal(combinedSignal)
    .then(data => {
        if (combinedSignal.aborted) return;
        statusEl.textContent = '加载成功';
        listEl.innerHTML = data.map(item => `<li>${item.name}</li>`).join('');
    })
    .catch(err => {
        if (combinedSignal.aborted) {
            if (timeoutSignal.aborted) {
                statusEl.textContent = '请求超时,已自动取消';
            } else if (cancelController.signal.aborted) {
                statusEl.textContent = '你主动取消了,不再处理结果';
            }
        } else {
            statusEl.textContent = '请求失败:' + err.message;
        }
    });
</script>

这个例子里,我模拟了一个需要3秒返回的接口。你如果点了“停止加载”按钮,请求立即取消,最终列表也不会出现数据。如果放任不管,5秒后超时信号会触发,同样是清掉定时器reject。因为我在then里加了 if (combinedSignal.aborted) return;,即便定时器已经被清掉导致resolve无效,也额外加了一层屏障。

真实fetch请求里的最佳实践

如果真是fetch而不是promise包裹,代码更短:

function loadUserList() {
    const userController = new AbortController();
    const timeoutSignal = AbortSignal.timeout(8000);
    const combined = AbortSignal.any([userController.signal, timeoutSignal]);

    fetch('/api/users', { signal: combined })
        .then(response => response.json())
        .then(list => renderList(list))
        .catch(error => {
            if (timeoutSignal.aborted) {
                message.error('网络超时,请稍后重试');
            } else if (userController.signal.aborted) {
                // 用户主动取消,不提示
            } else {
                message.error('请求失败');
            }
        });

    // 返回一个cancel函数,方便组件卸载时调用
    return () => userController.abort();
}

// 在React/Vue的效果函数里:
const cancel = loadUserList();
onCleanup(cancel);

这种写法和之前相比,好处就是组合信号是一次性创建的,不会丢。你不用把多个controller保存在模块级数组里,每个请求自己管理自己的“取消来源”,但又能共享同一个“总取消触发器”。

兼容你的React/Vue组件

React 18以上的useEffect里,我们可以直接用这个模式来取消副作用。例如加载用户列表并支持页面卸载时取消所有未完成请求:

import { useEffect } from 'react';

function UserList() {
    useEffect(() => {
        const controller = new AbortController();
        const timeoutSignal = AbortSignal.timeout(10000);
        const signal = AbortSignal.any([controller.signal, timeoutSignal]);

        fetch('/api/users', { signal })
            .then(r => r.json())
            .then(setUsers)
            .catch(e => {
                if (e.name === 'AbortError') return; // 被取消,不算错误
                console.error(e);
            });

        return () => controller.abort(); // 清理时取消
    }, []);
}

这里即使没有“手动取消”按钮,AbortSignal.timeout也给请求加了一道保险。以前你需要写一个useTimeoutRef然后手动clearTimeout,现在一个signal全搞定。

踩坑:AbortSignal.any的信号不能被直接复用

注意,AbortSignal.any返回的是一个全新的信号,不是某个原始信号。所以如果你把它用在fetch里,然后对它调用 signal.aborted,这个值会在任一上游信号aborted后变成true。但是——如果你想给一个详情页面同时发多个fetch,共享同一个abort事件,可以让这两个fetch都用同一个由AbortSignal.any创建出来的信号:

const cancelController = new AbortController();
const commonSignal = AbortSignal.any([cancelController.signal, AbortSignal.timeout(5000)]);

fetch(url1, { signal: commonSignal });
fetch(url2, { signal: commonSignal });

这样点击一个取消按钮两个请求都会断,十分好用。

兼容性到哪里了

  • Chrome / Edge:从116开始支持AbortSignal.any
  • Firefox:117开始支持
  • Safari:17.0开始支持
  • Node.js:20.3.0开始支持

如果你的项目还要兼容Chrome 115及之前的老浏览器,可以用一个简单的polyfill模拟:遍历signals数组,在任意一个触发‘abort’事件时调用新的abort。网上有很多现成实现,直接装上就可以。

实际感受

用了AbortSignal.any之后,原来在代码里到处找controller列表的回头路结束了。现在每个页面就只管当前页面需要的中止信号,再用timeout做兜底,通过any把两个信号合并。思路更加清晰,自己写起来也痛快。

代码是写给同事维护的,能不能少几个全局变量就少几个。以后再有“给我并行发几个请求,其中一个取消全部取消”的需求,一行AbortSignal.any就直接解决,值得一试。

JavaScript AbortSignal.any实战:一键取消多个fetch和超时,告别手动组合
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 javascript JavaScript AbortSignal.any实战:一键取消多个fetch和超时,告别手动组合 https://www.taomawang.com/web/javascript/2698.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务