前言
在现代 Web 开发中,我们经常需要处理异步操作,尤其是网络请求。有时,在请求完成之前,我们可能希望取消它。例如:
- 用户在请求完成前离开了当前页面。
- 用户发起了新的搜索请求,旧的搜索请求变得不再需要。
- 请求耗时过长,我们希望提供一个取消选项。
在 AbortController
出现之前,取消异步操作(如 XMLHttpRequest
请求)通常需要手动管理状态或使用一些不够直观的技巧。AbortController
提供了一种标准、简洁的方式来发出取消信号,并且可以与支持该机制的 Web API(如 fetch
)和 JavaScript API(如 Promises)集成。
什么是 AbortController
?
AbortController
是一个 Web API 接口,用于表示一个控制器对象,可以用来中断一个或多个 Web 请求或其他异步操作。
它主要由两个部分组成:
AbortController
对象本身: 这是你创建和控制取消过程的实例。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()
,该controller
和signal
就永久处于中断状态,后续对abort()
的调用将无效。
- 将关联的
AbortController
如何与 Promise 请求结合使用
许多现代的异步 Web API(如 fetch
)都支持接收一个 AbortSignal
作为选项。当你将一个 signal
传递给这些 API 时,它们内部会监听该信号的 abort
事件。一旦事件触发,API 就会尝试取消正在进行的异步操作,并且相关的 Promise 会以一个特定的错误(通常是 AbortError
)拒绝(reject)。
以下是如何结合 Promise (fetch
API) 和 AbortController
来取消请求的步骤和示例:
-
创建一个
AbortController
实例。JavaScriptconst controller = new AbortController();
-
获取与控制器关联的
signal
对象。JavaScriptconst signal = controller.signal;
-
在发起 Promise 请求时,将
signal
对象作为选项传递进去。 对于fetch
API,signal 作为第二个参数(options 对象)的signal
属性。JavaScriptconst fetchPromise = fetch(url, { signal: signal });
-
在需要取消请求时,调用
controller.abort()
方法。JavaScriptcontroller.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);
});
说明:
- 我们定义了一个
cancellableFetch
异步函数,它接收 URL 和一个AbortController
实例。 - 在函数内部,我们获取
controller.signal
并将其传递给fetch
函数的signal
选项。 - 使用
try...catch
块来处理fetch
Promise 的结果。 - 在
catch
块中,我们通过检查error.name === 'AbortError'
来判断错误是否是由于调用abort()
导致的取消。 - 在示例中,我们先发起一个请求,并使用
setTimeout
模拟在短时间内调用myController.abort()
来尝试取消该请求。 - 第二个示例展示了一个不会被取消的请求,以对比正常成功的情况。
通过这种方式,AbortController
提供了一个清晰、标准化的机制来控制和取消异步操作,使得代码更易于管理和理解。
关于 async 函数的返回值和 Promise
理解 async
函数的本质
-
async 函数总是返回一个 Promise:
在 JavaScript 中,任何被 async 关键字标记的函数,无论你在函数内部写不写 return 语句,它总是会返回一个 Promise 对象。
-
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 的微任务队列中继续执行。
- 如果
fetch
和response.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)的常见方式。
-
response 是什么?
await fetch(url, { signal: signal }) 这行代码执行成功后(表示服务器已经返回了响应头和部分响应),fetch 返回的 Promise 会 resolve 一个 Response 对象。这个 response 对象包含了服务器的响应信息,比如状态码 (response.status)、响应头 (response.headers) 等等。
-
响应体是流式的:
Response 对象本身并不直接包含响应体的数据(比如服务器返回的 JSON 或文本)。响应体数据是通过一个可读流(ReadableStream)来提供的,这样可以处理非常大的响应而不会一次性占用大量内存。
-
.json() 方法:
response.json() 是 Response 对象提供的一个异步方法。它的作用是:
- 读取响应体的完整内容。
- 将响应体的内容解析为 JSON 格式。
- 返回一个 新的 Promise 。这个 Promise 会在响应体被完全读取并成功解析为 JSON 后,以解析后的 JSON 数据作为值 resolve (成功) 。
-
为什么使用 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
变量。
所以,整个流程是:
fetch
发起请求,返回一个 Promise。await fetch
等待请求的 Promise 完成,得到Response
对象。response.json()
读取Response
的响应体并解析 JSON,返回一个新的 Promise。await response.json()
等待这个新的 Promise 完成,得到解析后的 JSON 数据。- 将解析后的 JSON 数据赋给
data
变量。 return data
使得最外层的cancellableFetch
返回的 Promise 以这个data
resolve。