跟着 MDN 学 HTML day_30:(AbortController 实现可取消的异步请求)

在现代 Web 开发中,异步操作随处可见,尤其是网络请求。但有时候我们需要主动取消一个正在进行的请求,比如用户切换了页面、重复提交表单、或者文件下载被中断。传统的做法往往难以优雅地处理这些场景。AbortController 的出现,为我们提供了一套标准化、可复用的中止机制。

一、AbortController 是什么

AbortController 是一个内置的 Web API,它允许我们主动取消一个或多个正在进行的异步操作。最典型的应用场景是与 Fetch API 配合,取消正在发送的网络请求。

它的核心设计思路是将"控制权"和"信号"分离:控制器负责发出中止指令,信号对象负责将这个指令传递给具体的异步操作。

javascript 复制代码
// 创建一个控制器实例
const controller = new AbortController();

// 获取与该控制器关联的信号对象
const signal = controller.signal;

// 监控信号是否被中止
signal.addEventListener('abort', () => {
  console.log('操作已被取消,原因为:', signal.reason);
});

// 在需要的时候调用 abort 方法
controller.abort();
// 输出: 操作已被取消,原因为:AbortError

需要注意的是,每个 AbortController 实例只能使用一次。一旦调用了 abort 方法,关联的 signal 状态就会变成"已中止",无法逆转。

二、构造函数与实例属性

AbortController 的 API 设计非常简洁。通过构造函数创建实例后,最常用的属性就是 signal,它返回一个 AbortSignal 对象,用于向异步操作传递取消信号。

javascript 复制代码
// 演示 signal 属性的使用
function createCancellableOperation() {
  const controller = new AbortController();
  const { signal } = controller;
  
  console.log('信号初始状态:', signal.aborted); // false
  
  // 设置一个定时器,5秒后自动取消
  const timeoutId = setTimeout(() => {
    controller.abort();
    console.log('信号中止后状态:', signal.aborted); // true
    console.log('中止原因:', signal.reason);
  }, 5000);
  
  return { controller, signal };
}

const { controller, signal } = createCancellableOperation();

// 可以在任意时刻主动取消
// controller.abort();

signal 对象还有一个 aborted 属性,用于判断操作是否已经被取消。在实际开发中,我们通常会将 signal 作为参数传递给支持取消机制的异步函数。

三、abort 方法的核心用法

abort 方法是 AbortController 的灵魂。调用它会触发 signal 的 abort 事件,同时将 signal.aborted 设置为 true。传入可选的 reason 参数可以更好地描述取消的原因。

javascript 复制代码
// 演示带原因的中止操作
async function fetchWithCustomAbort(url) {
  const controller = new AbortController();
  const signal = controller.signal;
  
  // 5秒后自动取消请求,并指定原因
  const timeoutId = setTimeout(() => {
    controller.abort('请求超时,已自动取消');
  }, 5000);
  
  try {
    const response = await fetch(url, { signal });
    clearTimeout(timeoutId);
    const data = await response.json();
    console.log('请求成功:', data);
    return data;
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('请求被取消,原因:', signal.reason);
    } else {
      console.log('请求出错:', error);
    }
  }
}

// 手动取消的示例
const manualController = new AbortController();
manualController.abort('用户点击了取消按钮');
// 此时 manualController.signal.reason 的值为 '用户点击了取消按钮'

从 98 版本开始的 Chrome 和 97 版本的 Firefox 开始,abort 方法支持传入自定义的 reason 参数,这大大方便了错误追踪和用户提示。

四、中止 Fetch 请求的完整示例

这是 AbortController 最经典的应用场景。当用户快速切换页面或者重复提交表单时,我们可以取消那些不再需要的请求,节省带宽并避免状态混乱。

javascript 复制代码
// 完整的视频下载控制示例
class VideoDownloader {
  constructor() {
    this.currentController = null;
    this.downloadBtn = document.getElementById('downloadBtn');
    this.abortBtn = document.getElementById('abortBtn');
    this.statusDiv = document.getElementById('status');
    
    this.downloadBtn.addEventListener('click', () => this.startDownload());
    this.abortBtn.addEventListener('click', () => this.cancelDownload());
  }
  
  async startDownload() {
    // 如果已有下载在进行,先取消它
    if (this.currentController) {
      this.currentController.abort('新的下载任务已启动');
    }
    
    this.currentController = new AbortController();
    const signal = this.currentController.signal;
    
    this.statusDiv.textContent = '下载中...';
    this.downloadBtn.disabled = true;
    this.abortBtn.disabled = false;
    
    try {
      const response = await fetch('/large-video.mp4', { signal });
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      
      const videoBlob = await response.blob();
      this.statusDiv.textContent = '下载完成!';
      console.log('视频大小:', (videoBlob.size / 1024 / 1024).toFixed(2), 'MB');
      
    } catch (error) {
      if (error.name === 'AbortError') {
        this.statusDiv.textContent = '下载已取消';
      } else {
        this.statusDiv.textContent = `下载失败: ${error.message}`;
      }
    } finally {
      this.downloadBtn.disabled = false;
      this.abortBtn.disabled = true;
      this.currentController = null;
    }
  }
  
  cancelDownload() {
    if (this.currentController) {
      this.currentController.abort('用户主动取消');
      console.log('已发送取消指令');
    }
  }
}

// 初始化下载器
new VideoDownloader();

当 abort 方法被调用时,fetch 返回的 Promise 会立即 reject,错误对象的名字为 AbortError。通过捕获这个特定错误,我们可以给用户提供清晰的状态反馈。

五、同时中止多个请求

AbortController 的一个强大特性是一个控制器可以关联多个异步操作。这意味着我们可以用一个"总开关"同时取消多个请求。

javascript 复制代码
// 批量请求的中止管理
async function searchProducts(keyword) {
  const controller = new AbortController();
  const signal = controller.signal;
  
  // 同时发起三个相关的 API 请求
  const endpoints = [
    `/api/products?q=${keyword}`,
    `/api/categories?q=${keyword}`,
    `/api/recommendations?q=${keyword}`
  ];
  
  const promises = endpoints.map(endpoint => 
    fetch(endpoint, { signal })
      .then(res => res.json())
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(`${endpoint} 请求失败:`, err);
        }
        return null;
      })
  );
  
  // 设置超时自动取消
  const timeoutId = setTimeout(() => {
    controller.abort('搜索超时');
    console.log('已取消所有搜索请求');
  }, 3000);
  
  try {
    const results = await Promise.all(promises);
    clearTimeout(timeoutId);
    
    const [products, categories, recommendations] = results;
    return { products, categories, recommendations };
    
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('搜索请求已取消');
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

// 使用示例
let currentSearch = null;

async function handleSearch() {
  // 清除之前的搜索
  if (currentSearch) {
    currentSearch.abort();
  }
  
  const keyword = document.getElementById('searchInput').value;
  const controller = new AbortController();
  currentSearch = controller;
  
  try {
    const results = await searchProducts(keyword);
    console.log('搜索结果:', results);
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('搜索出错:', error);
    }
  } finally {
    if (currentSearch === controller) {
      currentSearch = null;
    }
  }
}

这种模式在实现搜索建议、数据预取、或者需要清理旧请求的场景中非常实用。

六、浏览器兼容性与注意事项

AbortController 在现代浏览器中得到了广泛支持。Chrome 66、Firefox 57、Safari 12.1、Edge 16 及以上版本都完全支持。Node.js 从 14.17 版本开始也支持了这个 API。

在使用时需要注意以下几点:每个控制器只能使用一次,中止后无法重置;不是所有异步操作都支持 AbortSignal,目前原生支持的主要是 Fetch API、某些流操作以及部分 Node.js API;在取消请求后,服务器端可能仍在处理请求,取消只影响客户端等待响应的行为。

javascript 复制代码
// 兼容性处理示例
function createCancellableFetch(url, options = {}) {
  // 检测是否支持 AbortController
  if (typeof AbortController === 'undefined') {
    console.warn('当前环境不支持 AbortController,将发起不可取消的请求');
    return fetch(url, options);
  }
  
  const controller = new AbortController();
  const request = fetch(url, { ...options, signal: controller.signal });
  
  // 返回增强的 Promise,附加 cancel 方法
  const cancellablePromise = request;
  cancellablePromise.cancel = () => controller.abort();
  
  return cancellablePromise;
}

// 使用示例
const fetchTask = createCancellableFetch('/api/data');
fetchTask.catch(error => {
  if (error.name === 'AbortError') {
    console.log('用户取消了请求');
  }
});

// 需要取消时
// fetchTask.cancel();

AbortController 的出现让前端开发者终于有了一个标准化的方式来取消异步操作。掌握它的用法,可以让我们在处理复杂用户交互时写出更加健壮和优雅的代码。


想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!

相关推荐
前端若水1 小时前
选择器的威力 —— :has()、@layer、原生嵌套
前端·css·css3
nashane1 小时前
HarmonyOS 6学习:Web组件本地资源跨域访问全解析与实战
前端·学习·harmonyos·harmonyos 5
小陈同学,,1 小时前
地图第一次进来慢的问题二
前端
阿赛工作室2 小时前
基于Vue3和TensorFlow.js的数字图像识别应用HTML单文件
javascript·html·tensorflow
万少2 小时前
公测期 0 元/月!商汤 SenseNova 免费 Token 再不领就没了
前端·javascript·后端
Hello--_--World2 小时前
Webpack:Webpack 核心配置、什么是 Loader? 什么是plugin?webpack 构建流程
前端·webpack·node.js
优联前端2 小时前
什么是 GEO?SEO对比GEO,如何做好 GEO?怎么验证 GEO 效果?
前端·人工智能·用户体验·geo·seo优化·优联前端
时间不早了sss2 小时前
Python处理文档
开发语言·前端·python
Json____2 小时前
前端入门练习题集-HTML/CSS/JS实战小项目15个
前端·css·html