各位观众老爷,大家好!今天咱们来聊聊 JavaScript 里一对好基友:await
和 Promise.race
,看看它们是怎么联手搞定超时控制这个小妖精的。
1. 故事的开端:为啥需要超时控制?
想象一下,你写了个程序,要从服务器获取数据。结果呢?服务器它老人家抽风了,半天没反应。你的程序傻乎乎地在那儿等着,用户急得抓耳挠腮。这可不行!用户体验至上,咱们得给它设个时限,免得一直卡死。这就是超时控制的意义所在。
2. await
:等等我,Promise!
await
关键字是 JavaScript 里的“暂停”按钮。它只能在 async
函数中使用,作用是等待一个 Promise
对象 resolve 或 reject。
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log('Data:', data);
} catch (error) {
console.error('Error fetching data:', error);
}
}
fetchData();
这段代码里,await fetch(...)
会暂停函数的执行,直到 fetch
返回的 Promise
resolve。拿到 response
后,await response.json()
又会暂停,直到 JSON 解析完成。如果 fetch
或 response.json()
reject 了,就会进入 catch
代码块。
3. Promise.race
:赛跑开始!
Promise.race
就像一场短跑比赛,给它一组 Promise
,哪个先 resolve 或 reject,就返回哪个的结果。其他 Promise
就算后面跑得再快,也没用了。
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Promise 1 resolved'), 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Promise 2 resolved'), 500);
});
Promise.race([promise1, promise2])
.then(result => console.log('Race winner:', result)) // 输出:Race winner: Promise 2 resolved
.catch(error => console.error('Race error:', error));
在这个例子里,promise2
先 resolve,所以 Promise.race
返回 promise2
的结果。
4. 超时控制的完美组合:await
+ Promise.race
现在,咱们把 await
和 Promise.race
组合起来,实现超时控制。
async function fetchDataWithTimeout(url, timeout) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), timeout);
});
const fetchPromise = fetch(url);
try {
const response = await Promise.race([fetchPromise, timeoutPromise]);
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data with timeout:', error);
throw error; // 重新抛出异常,让调用者知道发生了错误
}
}
// 使用示例
fetchDataWithTimeout('https://api.example.com/data', 2000) // 2秒超时
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));
这段代码的核心在于:
timeoutPromise
: 创建一个Promise
,在指定的时间后 reject,并抛出一个Timeout
错误。Promise.race([fetchPromise, timeoutPromise])
: 让fetchPromise
和timeoutPromise
赛跑。如果fetchPromise
先 resolve,就返回response
,然后解析 JSON。如果timeoutPromise
先 reject,就进入catch
代码块,抛出Timeout
错误。- 重新抛出异常 (
throw error
): 在catch
块中,重新抛出捕获到的错误。这是为了让调用fetchDataWithTimeout
函数的代码也能知道发生了超时或其他错误,并且可以采取相应的处理措施。如果只是简单地console.error
打印错误信息,调用者就无法得知错误,这可能会导致程序出现意料之外的行为。
流程图:
graph TD
A[开始] --> B{创建 timeoutPromise};
B --> C{创建 fetchPromise};
C --> D{Promise.race([fetchPromise, timeoutPromise])};
D -- fetchPromise resolve --> E{await response.json()};
E --> F{返回 data};
D -- timeoutPromise reject --> G{catch (error)};
G --> H{抛出 error};
F --> I[结束];
H --> I;
表格:
组件 | 描述 |
---|---|
timeoutPromise |
一个在指定时间后 reject 的 Promise,用于模拟超时。 |
fetchPromise |
发起网络请求的 Promise。 |
Promise.race |
让 fetchPromise 和 timeoutPromise 赛跑,哪个先完成就返回哪个的结果。 |
await |
等待 Promise.race 返回的 Promise 完成,拿到结果。 |
try...catch |
捕获 Promise.race 抛出的错误,例如超时错误。 |
throw error |
重新抛出错误, 允许调用者处理错误。 |
5. 进阶技巧:AbortController
除了 Promise.race
,还可以使用 AbortController
来取消 fetch
请求,也能达到超时控制的效果。 AbortController
提供了一种取消 DOM
操作(包括 fetch
请求)的方法。
async function fetchDataWithAbortController(url, timeout) {
const controller = new AbortController();
const signal = controller.signal;
const fetchPromise = fetch(url, { signal });
const timeoutId = setTimeout(() => {
controller.abort(); // 取消 fetch 请求
console.log('Fetch aborted due to timeout.');
}, timeout);
try {
const response = await fetchPromise;
clearTimeout(timeoutId); // 清除定时器,防止重复取消
const data = await response.json();
return data;
} catch (error) {
clearTimeout(timeoutId); // 清除定时器,防止重复取消
console.error('Error fetching data:', error);
throw error; // 重新抛出异常
}
}
// 使用示例
fetchDataWithAbortController('https://api.example.com/data', 2000)
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));
这段代码的关键在于:
AbortController
: 创建一个AbortController
对象,用于控制fetch
请求的取消。signal
: 通过controller.signal
获取一个AbortSignal
对象,并将其传递给fetch
的 options。controller.abort()
: 在超时后,调用controller.abort()
取消fetch
请求。clearTimeout(timeoutId)
: 当请求成功完成或者出现错误时,清除timeoutId
,避免重复调用controller.abort()
。
流程图:
graph TD
A[开始] --> B{创建 AbortController};
B --> C{创建 fetchPromise (带 signal)};
C --> D{setTimeout (设置 timeout)};
D --> E{await fetchPromise};
E -- 请求成功 --> F{clearTimeout};
F --> G{await response.json()};
G --> H{返回 data};
E -- 请求失败 --> I{catch (error)};
I --> J{clearTimeout};
J --> K{抛出 error};
D -- 超时 --> L{controller.abort()};
L --> I;
H --> M[结束];
K --> M;
表格:
组件 | 描述 |
---|---|
AbortController |
用于取消 DOM 操作(包括 fetch 请求)。 |
AbortSignal |
与 fetch 请求关联,用于接收取消信号。 |
fetch(url, {signal}) |
发起网络请求,并将 AbortSignal 传递给它,以便在需要时取消请求。 |
setTimeout |
设置一个定时器,在指定时间后调用 controller.abort() 取消请求。 |
controller.abort() |
取消 fetch 请求。 |
clearTimeout |
清除定时器,防止重复取消请求。 |
try...catch |
捕获 fetch 抛出的错误,例如取消错误。 |
throw error |
重新抛出错误, 允许调用者处理错误。 |
6. 两种方法的比较
特性 | Promise.race |
AbortController |
---|---|---|
取消请求 | 无法直接取消 fetch 请求,只是忽略超时后返回的 Promise 。 |
可以直接取消 fetch 请求,节省服务器资源。 |
兼容性 | 较好,大部分现代浏览器都支持。 | 较好,大部分现代浏览器都支持。 |
代码复杂度 | 相对简单。 | 相对复杂,需要处理 AbortController 和 AbortSignal 。 |
适用场景 | 不需要真正取消请求,只需要在超时后忽略结果。 | 需要真正取消请求,例如用户取消了请求或者网络环境发生变化。 |
资源消耗 | 即使超时,请求也会继续执行到完成,消耗服务器资源。 | 超时后,请求会被取消,节省服务器资源。 |
错误处理 | fetch 失败或超时都会抛出错误,需要在 catch 块中处理。 |
fetch 失败或超时都会抛出错误,需要在 catch 块中处理。 当 fetch 被 AbortController 中止时,通常会抛出一个 AbortError 类型的错误,需要进行额外判断。 |
7. 实战演练:一个更健壮的超时控制函数
为了让我们的超时控制函数更健壮,可以添加一些额外的处理:
async function fetchDataWithTimeoutRobust(url, timeout) {
const controller = new AbortController();
const signal = controller.signal;
let timeoutId;
try {
const fetchPromise = fetch(url, { signal });
timeoutId = setTimeout(() => {
controller.abort();
console.log('Fetch aborted due to timeout.');
}, timeout);
const response = await fetchPromise;
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
console.log('Fetch aborted by the user or timeout.'); // 更友好的提示
}
console.error('Error fetching data:', error);
throw error; // 重新抛出异常
} finally {
// 无论成功还是失败,都要执行的操作
console.log('Fetch operation completed (success or failure).');
}
}
// 使用示例
fetchDataWithTimeoutRobust('https://api.example.com/data', 2000)
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));
在这个版本中,我们做了以下改进:
- 错误状态码检查: 添加了
response.ok
检查,如果 HTTP 状态码不是 200-299 范围,就抛出一个错误。 AbortError
处理: 在catch
块中,判断错误类型是否为AbortError
,如果是,就打印一个更友好的提示信息。finally
块: 添加了一个finally
块,无论try
块中的代码是否成功执行,finally
块中的代码都会执行。这可以用于执行一些清理操作,例如关闭数据库连接或释放资源。
8. 总结
今天,我们学习了如何使用 await
和 Promise.race
,以及 AbortController
来实现超时控制。Promise.race
简单易用,但无法真正取消请求。AbortController
可以取消请求,更节省资源,但也更复杂一些。 选择哪种方法,取决于你的具体需求。
希望今天的分享能帮助你更好地掌握 JavaScript 的异步编程。 记住,熟练掌握这些技巧,才能写出更健壮、更用户友好的代码。
咱们下次再见!