Kodokon kodokon.com

深入异步:promise、async/await、错误处理

驾驭事件循环,正确地并行化你的 promise,并让异步错误处理变得可靠。

11 分钟 · 3 题

在 Kodokon 中打开本课

执行模型依赖于一个宏任务队列(setTimeout、I/O)和一个微任务队列(.thenawaitqueueMicrotask)。在每个任务之后,引擎会在继续之前彻底清空微任务队列。直接后果是:一个已解决的 promise 总是先于 setTimeout(fn, 0) 运行,而一条过长的微任务链条完全可能像同步循环一样冻结渲染。

JAVASCRIPT
console.log("start");
setTimeout(() => console.log("macrotask"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("end");
// start, end, microtask, macrotask
微任务先于任何待处理的宏任务运行。

async/await 是构建在 promise 之上的语法糖,但它隐藏着一个性能陷阱:循环内部的每个 await 都会把这些操作串行化。如果这些请求是相互独立的,就先把它们全部发起,然后用 Promise.all 等待整批完成。对于三个各 100 毫秒的请求,串行版本要花 300 毫秒,而并行版本只要 100 毫秒。

JAVASCRIPT
const fetchUser = (id) => new Promise((res) => {
  setTimeout(() => res({ id }), 100);
});
async function loadSequential(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id)); // ~100 ms each
  }
  return users;
}
async function loadParallel(ids) {
  return Promise.all(ids.map(fetchUser)); // ~100 ms total
}
结果相同,延迟却被除以了调用的次数。

根据你所需要的失败契约来挑选合适的组合器:Promise.all第一个失败时就拒绝(全有或全无),allSettled 会等待所有人并描述每一个结果,race 取第一个敲定的结果(无论成功或失败,适合用于超时),any 取第一个成功的结果,只有在全部失败时才拒绝。对于一个必须展示部分结果的仪表盘,allSettled 几乎总是正确的选择。

JAVASCRIPT
const tasks = [
  Promise.resolve("profile loaded"),
  Promise.reject(new Error("network unavailable")),
];
const results = await Promise.allSettled(tasks);
for (const r of results) {
  if (r.status === "fulfilled") {
    console.log("success:", r.value);
  } else {
    console.error("failure:", r.reason.message);
  }
}
每一个结果都被描述出来;没有哪个失败会遮住其他结果。

知识检测

确认你已牢记本课的重点内容。

  1. 一个同步的 console.log、一个 Promise.resolve().then 回调和一个 setTimeout(fn, 0) 回调,会按什么顺序运行?
    • 同步、setTimeout、then
    • 同步、then、setTimeout
    • then、同步、setTimeout
  2. 你发起了十个相互独立的请求。哪种方式能把总延迟降到最低?
    • 在 for...of 循环里每个请求一个 await
    • 先创建所有 promise,然后 await Promise.all
    • 把各个 .then 调用一个接一个地链起来
    • 用 setTimeout 把这些调用错开时间
  3. 为什么没有 await 的 try { doAsync(); } catch (e) {} 无法捕获 doAsync 的拒绝?
    • 因为 catch 在 async 函数内部不起作用
    • 因为拒绝发生在 try 块已经退出之后,毕竟这个 promise 从未被 await
    • 因为 promise 只能用 .catch 来捕获