JavaScript内核与高级编程之:`JavaScript` 的 `await` 关键字与 `Promise.race`:如何实现超时控制。

各位观众老爷,大家好!今天咱们来聊聊 JavaScript 里一对好基友:awaitPromise.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 解析完成。如果 fetchresponse.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

现在,咱们把 awaitPromise.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]): 让 fetchPromisetimeoutPromise 赛跑。如果 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 fetchPromisetimeoutPromise 赛跑,哪个先完成就返回哪个的结果。
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 请求,节省服务器资源。
兼容性 较好,大部分现代浏览器都支持。 较好,大部分现代浏览器都支持。
代码复杂度 相对简单。 相对复杂,需要处理 AbortControllerAbortSignal
适用场景 不需要真正取消请求,只需要在超时后忽略结果。 需要真正取消请求,例如用户取消了请求或者网络环境发生变化。
资源消耗 即使超时,请求也会继续执行到完成,消耗服务器资源。 超时后,请求会被取消,节省服务器资源。
错误处理 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. 总结

今天,我们学习了如何使用 awaitPromise.race,以及 AbortController 来实现超时控制。Promise.race 简单易用,但无法真正取消请求。AbortController 可以取消请求,更节省资源,但也更复杂一些。 选择哪种方法,取决于你的具体需求。

希望今天的分享能帮助你更好地掌握 JavaScript 的异步编程。 记住,熟练掌握这些技巧,才能写出更健壮、更用户友好的代码。

咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注