AbortController 详解:如何优雅地取消异步操作

前言

在现代 Web 开发中,我们经常需要处理异步操作,尤其是网络请求。有时,在请求完成之前,我们可能希望取消它。例如:

  • 用户在请求完成前离开了当前页面。
  • 用户发起了新的搜索请求,旧的搜索请求变得不再需要。
  • 请求耗时过长,我们希望提供一个取消选项。

AbortController 出现之前,取消异步操作(如 XMLHttpRequest 请求)通常需要手动管理状态或使用一些不够直观的技巧。AbortController 提供了一种标准、简洁的方式来发出取消信号,并且可以与支持该机制的 Web API(如 fetch)和 JavaScript API(如 Promises)集成。

什么是 AbortController

AbortController 是一个 Web API 接口,用于表示一个控制器对象,可以用来中断一个或多个 Web 请求或其他异步操作。

它主要由两个部分组成:

  1. AbortController 对象本身: 这是你创建和控制取消过程的实例。
  2. AbortSignal 对象: 这是 AbortController 的一个属性 (controller.signal)。它是一个信号源,你可以将这个信号传递给任何支持 AbortSignal 的异步 API。当你在 AbortController 实例上调用 abort() 方法时,这个 signal 对象就会被标记为已中断,并会触发一个 abort 事件。

AbortController 的核心组件

  • new AbortController(): 用于创建一个新的 AbortController 实例。

  • AbortController.signal: 返回与该控制器关联的 AbortSignal 对象。这个 signal 就是用来向异步操作传递取消信号的媒介。AbortSignal 对象具有以下重要特性:

    • aborted (boolean): 一个只读属性,表示该信号是否已经被触发了中断。初始值为 false,调用 abort() 后变为 true
    • onabort: 一个事件处理属性,可以为一个函数赋值,当 abort 事件发生时执行。
    • addEventListener('abort', handler): 允许你注册一个事件监听器,当 abort 事件被分派到 signal 对象时调用指定的处理函数。
  • AbortController.abort(): 调用此方法将中止与该控制器关联的所有异步操作。调用 abort() 会执行以下操作:

    • 将关联的 signal.aborted 属性设置为 true
    • 向关联的 signal 对象分派一个名为 abort 的事件。
    • 一旦调用了 abort(),该 controllersignal 就永久处于中断状态,后续对 abort() 的调用将无效。

AbortController 如何与 Promise 请求结合使用

许多现代的异步 Web API(如 fetch)都支持接收一个 AbortSignal 作为选项。当你将一个 signal 传递给这些 API 时,它们内部会监听该信号的 abort 事件。一旦事件触发,API 就会尝试取消正在进行的异步操作,并且相关的 Promise 会以一个特定的错误(通常是 AbortError)拒绝(reject)。

以下是如何结合 Promise (fetch API) 和 AbortController 来取消请求的步骤和示例:

  1. 创建一个 AbortController 实例。

    JavaScript 复制代码
    const controller = new AbortController();
  2. 获取与控制器关联的 signal 对象。

    JavaScript 复制代码
    const signal = controller.signal;
  3. 在发起 Promise 请求时,将 signal 对象作为选项传递进去。 对于 fetch API,signal 作为第二个参数(options 对象)的 signal 属性。

    JavaScript 复制代码
    const fetchPromise = fetch(url, { signal: signal });
  4. 在需要取消请求时,调用 controller.abort() 方法。

    JavaScript 复制代码
    controller.abort();

controller.abort() 被调用时:

  • signal.aborted 变为 true
  • signal 触发 abort 事件。
  • fetchPromise 会被拒绝,拒绝的原因是一个 AbortError 对象。

你需要捕获这个错误来判断请求是否是被取消了。

结合 Promise 请求取消请求的示例

JavaScript 复制代码
/**
 * 异步发起一个可取消的 Fetch 请求
 * @param {string} url - 请求的 URL
 * @param {Object} options - fetch 选项,可以包含 signal
 * @param {AbortController} controller - 用于控制取消的 AbortController 实例
 * @returns {Promise<Response>} - Fetch API 返回的 Promise
 */
async function cancellableFetch(url, controller) {
    // 确保传入了 controller
    if (!(controller instanceof AbortController)) {
        throw new Error("必须提供一个 AbortController 实例");
    }

    const signal = controller.signal;

    try {
        console.log(`正在发起请求: ${url}`);
        // 将 signal 传递给 fetch
        const response = await fetch(url, { signal: signal });

        // 请求成功完成
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        console.log("请求成功,接收到数据:", data);
        return data;

    } catch (error) {
        // 检查错误是否是 AbortError
        if (error.name === 'AbortError') {
            console.log(`请求已被取消: ${url}`);
            // 可以选择在这里不抛出错误,或者抛出更具体的错误
            throw new Error(`请求被取消: ${url}`);
        } else {
            // 其他类型的错误 (例如网络错误)
            console.error(`请求发生错误: ${url}`, error);
            throw error; // 重新抛出其他错误
        }
    }
}

// --- 示例使用 ---

// 1. 创建 AbortController
const myController = new AbortController();
const requestUrl = 'https://jsonplaceholder.typicode.com/todos/1'; // 示例 API

// 2. 发起请求
const requestPromise = cancellableFetch(requestUrl, myController);

// 3. 假设我们在 50 毫秒后取消请求 (模拟用户操作或超时)
const cancelTimer = setTimeout(() => {
    console.log("正在调用 abort()...");
    myController.abort(); // 调用 abort() 取消请求
}, 50); // 尝试在请求完成前取消

requestPromise
    .then(data => {
        console.log("Promise resolved:", data);
        clearTimeout(cancelTimer); // 如果请求成功,清除取消定时器
    })
    .catch(error => {
        console.error("Promise rejected:", error.message);
        // 注意:这里的 catch 块会捕获 AbortError 或其他错误
    });

// --- 另一个示例:让请求成功完成 ---
console.log("\n--- 发起另一个请求,这次不取消 ---");
const anotherController = new AbortController();
const anotherRequestUrl = 'https://jsonplaceholder.typicode.com/posts/1';

cancellableFetch(anotherRequestUrl, anotherController)
    .then(data => {
        console.log("第二个 Promise resolved:", data);
    })
    .catch(error => {
        console.error("第二个 Promise rejected:", error.message);
    });

说明:

  1. 我们定义了一个 cancellableFetch 异步函数,它接收 URL 和一个 AbortController 实例。
  2. 在函数内部,我们获取 controller.signal 并将其传递给 fetch 函数的 signal 选项。
  3. 使用 try...catch 块来处理 fetch Promise 的结果。
  4. catch 块中,我们通过检查 error.name === 'AbortError' 来判断错误是否是由于调用 abort() 导致的取消。
  5. 在示例中,我们先发起一个请求,并使用 setTimeout 模拟在短时间内调用 myController.abort() 来尝试取消该请求。
  6. 第二个示例展示了一个不会被取消的请求,以对比正常成功的情况。

通过这种方式,AbortController 提供了一个清晰、标准化的机制来控制和取消异步操作,使得代码更易于管理和理解。

关于 async 函数的返回值和 Promise

理解 async 函数的本质

  1. async 函数总是返回一个 Promise:

    在 JavaScript 中,任何被 async 关键字标记的函数,无论你在函数内部写不写 return 语句,它总是会返回一个 Promise 对象。

  2. async 函数内部的 return 语句决定了返回的 Promise 最终会以什么值"成功"(resolve):

    • 如果在 async 函数内部,代码执行到 return value; 并且没有抛出错误,那么这个 async 函数返回的 Promise 就会以 value 这个值成功(resolve)
    • 如果在 async 函数内部,发生了未捕获的错误(无论是用 throw 主动抛出,还是 await 后面的 Promise 被拒绝),那么这个 async 函数返回的 Promise 就会以这个错误为原因失败(reject)

回到我的代码:

JavaScript 复制代码
async function cancellableFetch(url, controller) {
    // ... 代码 ...
    try {
        // ... 代码 ...
        const response = await fetch(url, { signal: signal });
        // ... 代码 ...
        const data = await response.json(); 
        console.log("请求成功,接收到数据:", data);
        return data; // <--- 这里的 return data
        // ... 代码 ...
    } catch (error) {
        // 如果发生错误,这个 Promise 会在这里 reject
        // ... 代码 ...
        throw error;
    }
}

// 外部调用
const requestPromise = cancellableFetch(requestUrl, myController); // <--- requestPromise 接收的是 cancellableFetch 返回的 Promise 对象

当我调用 cancellableFetch(requestUrl, myController) 时,函数会立即开始执行,并且 立即返回一个 Promise 对象 。这个 Promise 对象被赋给了 requestPromise 这个变量。

函数内部的异步操作(await fetch(...)await response.json())会在 Promise 的微任务队列中继续执行。

  • 如果 fetchresponse.json() 都成功了,并且没有发生其他错误,那么代码会执行到 return data; 这一行。此时,cancellableFetch 返回的那个 Promise (requestPromise 所指向的) 就会以 data 这个值 resolve (成功)
  • 如果在这个过程中发生错误(包括网络错误或被 AbortController 取消),catch 块会被执行,并且其中的 throw error; 会导致 cancellableFetch 返回的那个 Promise reject (失败)

所以,requestPromise 变量里存储的不是最终的数据 data 本身,而是一个 Promise 对象 ,这个 Promise 代表了异步操作的结果 (可能是成功时的 data,也可能是失败时的错误)。我需要使用 .then() 方法来获取 Promise 成功时返回的 data,使用 .catch() 方法来处理 Promise 失败时(包括取消)的错误。

JavaScript 复制代码
requestPromise
    .then(data => { // Promise 成功时,这里的 data 就是 cancellableFetch 内部 return 的那个 data
        console.log("Promise resolved:", data);
    })
    .catch(error => { // Promise 失败时,这里的 error 是 cancellableFetch 内部 throw 或 await 拒绝的错误
        console.error("Promise rejected:", error.message);
    });

这正是 Promise 的工作方式:发起异步操作(函数立即返回 Promise),然后在 Promise 上注册回调函数 (.then().catch()) 来处理异步操作最终的结果。

关于 const data = await response.json(); 用法

这行代码是处理 fetch 请求返回的响应体(Response body)的常见方式。

  1. response 是什么?

    await fetch(url, { signal: signal }) 这行代码执行成功后(表示服务器已经返回了响应头和部分响应),fetch 返回的 Promise 会 resolve 一个 Response 对象。这个 response 对象包含了服务器的响应信息,比如状态码 (response.status)、响应头 (response.headers) 等等。

  2. 响应体是流式的:

    Response 对象本身并不直接包含响应体的数据(比如服务器返回的 JSON 或文本)。响应体数据是通过一个可读流(ReadableStream)来提供的,这样可以处理非常大的响应而不会一次性占用大量内存。

  3. .json() 方法:

    response.json() 是 Response 对象提供的一个异步方法。它的作用是:

    • 读取响应体的完整内容。
    • 将响应体的内容解析为 JSON 格式。
    • 返回一个 新的 Promise 。这个 Promise 会在响应体被完全读取并成功解析为 JSON 后,以解析后的 JSON 数据作为值 resolve (成功)
  4. 为什么使用 await?

    因为 response.json() 方法返回的是一个 Promise,读取和解析响应体是一个异步操作,所以我们需要使用 await 关键字来等待这个 Promise 完成。await response.json() 会暂停 async 函数的执行,直到 response.json() 返回的 Promise resolve。Promise resolve 后,await 会取出 Promise 成功的值(即解析后的 JSON 数据),并将这个值赋给 const data 变量。

简单来说,const data = await response.json(); 这行代码的作用就是:等待从网络接收到的响应体数据,并将其按照 JSON 格式解析后,把解析出来的 JavaScript 对象赋值给 data 变量。

所以,整个流程是:

  1. fetch 发起请求,返回一个 Promise。
  2. await fetch 等待请求的 Promise 完成,得到 Response 对象。
  3. response.json() 读取 Response 的响应体并解析 JSON,返回一个新的 Promise。
  4. await response.json() 等待这个新的 Promise 完成,得到解析后的 JSON 数据。
  5. 将解析后的 JSON 数据赋给 data 变量。
  6. return data 使得最外层的 cancellableFetch 返回的 Promise 以这个 data resolve。
相关推荐
小小小小宇12 分钟前
PC和WebView白屏检测
前端
天天扭码24 分钟前
ES6 Symbol 超详细教程:为什么它是避免对象属性冲突的终极方案?
前端·javascript·面试
小矮马27 分钟前
React-组件和props
前端·javascript·react.js
懒羊羊我小弟31 分钟前
React Router v7 从入门到精通指南
前端·react.js·前端框架
DC...1 小时前
vue滑块组件设计与实现
前端·javascript·vue.js
Mars狐狸1 小时前
AI项目改用服务端组件实现对话?包体积减小50%!
前端·react.js
H5开发新纪元1 小时前
Vite 项目打包分析完整指南:从配置到优化
前端·vue.js
嘻嘻嘻嘻嘻嘻ys1 小时前
《Vue 3.3响应式革新与TypeScript高效开发实战指南》
前端·后端
恋猫de小郭2 小时前
腾讯 Kuikly 正式开源,了解一下这个基于 Kotlin 的全平台框架
android·前端·ios
2301_799404912 小时前
如何修改npm的全局安装路径?
前端·npm·node.js