极简工具箱

免费在线工具 · 本地计算 · 无需上传

Promise 与 async/await:执行顺序和四个常见错误

「为什么 await 之后代码还没执行」「为什么 for 循环里 await 慢得离谱」——异步的坑大多不是语法问题,而是对执行顺序的误解。

广告位(百度联盟)

一、先看一段输出题

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出:1 4 3 2

原因:同步代码先执行完(1、4),然后清空微任务队列(Promise.then → 3),最后才轮到宏任务(setTimeout → 2)。setTimeout(fn, 0) 不是「立刻」,而是「下一轮宏任务」。

await 的本质也是微任务:await 后面的代码会被放进微任务队列,等当前同步代码跑完再继续。

二、Promise 的三个状态

状态一旦改变就不可逆,且只能改一次。这带来一个重要特性:Promise 可以被多次 then,但结果只算一次。

const p = fetch('/api/user');
p.then(r => r.json()).then(console.log);
p.catch(console.error);   // 同一个 p,可以挂多个处理

三、async/await 只是语法糖

async function load() {
  const res = await fetch('/api/user');   // 等待 Promise 落定
  return res.json();                       // 返回值会被包成 Promise
}

要点:

四、四个高频错误

1. 在循环里串行 await

// 慢:10 个请求依次等,总耗时 = 累加
for (const id of ids) { await fetch('/api/' + id); }

// 快:并发发出,总耗时 ≈ 最慢的那个
await Promise.all(ids.map(id => fetch('/api/' + id)));

如果必须限流(比如最多 5 个并发),要自己写池子,而不是改回串行。

2. 忘了 catch

await 抛出的异常会中断当前函数,如果调用方也没包 try/catch,就变成 unhandled rejection。建议:

try {
  const data = await load();
} catch (e) {
  console.error('加载失败', e);
}

3. Promise.all 的「一票否决」

Promise.all 只要有一个 reject 就整体 reject,其它请求仍在跑但结果被丢弃。需要「全部结果都要,不管成功失败」时用 Promise.allSettled:

const results = await Promise.allSettled(tasks);
const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value);

4. forEach 里的 await 不生效

arr.forEach(async item => { await save(item); });  // 外层不会等待

forEach 不关心回调返回值。要顺序执行用 for...of,要并发用 map + Promise.all。

五、并发控制的小工具

async function pool(items, limit, worker) {
  const ret = [];
  let i = 0;
  const runners = Array.from({ length: limit }, async () => {
    while (i < items.length) {
      const idx = i++;
      ret[idx] = await worker(items[idx]);
    }
  });
  await Promise.all(runners);
  return ret;
}

这是爬取、批量上传场景的常用模式:控制并发数,避免把对方打挂或本地连接耗尽。

六、超时与取消

fetch 本身没有超时,需要 AbortController:

const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 8000);
try {
  const res = await fetch(url, { signal: ctrl.signal });
} finally {
  clearTimeout(t);
}

注意 finally 里清定时器,否则请求成功后定时器仍会在 8 秒后触发 abort(虽然已无影响,但会干扰调试)。

七、调试建议

排查异步问题时,先确认「这段代码属于同步、微任务还是宏任务」,再检查是否漏了 await、是否在 map 里返回了 Promise。多段异步代码先后顺序混乱时,在每段前后打时间戳日志,比盯着代码猜快得多。

小结

同步 → 微任务 → 宏任务;await 本质是微任务;循环里并发用 Promise.all,需要全部结果用 allSettled,forEach 不会等异步;fetch 超时用 AbortController。

本文相关工具(本地计算,无需上传)

更多使用教程