前端深入

从 Promise.all 深入理解 Promise 链和微任务

这篇笔记记录一下我今天重新梳理 Promise 的过程。 起因是在看自己 Next.js 项目首页时,看到了一段代码: 这段代码本身不难理解。 它的意思是并发读取文章、项目、爱好和统计数量,全部完成以后再继续渲染页面。 但我继续想下去以后,发现 Promise 里有很多以前学过但没有真正想透的细节: 今天正好没什么事,

2026年6月17日22 分钟阅读
从 Promise.all 深入理解 Promise 链和微任务

这篇笔记记录一下我今天重新梳理 Promise 的过程。

起因是在看自己 Next.js 项目首页时,看到了一段代码:

ts
const [articles, projects, hobbies, contentCounts] = await Promise.all([
  getPublishedArticles(),
  getPublishedProjects(),
  getPublishedHobbies(),
  getPublishedContentCounts(),
]);

这段代码本身不难理解。

它的意思是并发读取文章、项目、爱好和统计数量,全部完成以后再继续渲染页面。

但我继续想下去以后,发现 Promise 里有很多以前学过但没有真正想透的细节:

text
Promise.all 为什么能保持结果顺序?
Promise.resolve(item) 为什么可以同时处理普通值和 Promise?
then 里面到底做了什么?
then 为什么会返回一个新的 Promise?
resolve 一个 Promise 时,为什么外层 Promise 会跟随内层 Promise?
Promise 链执行时,微任务之间会不会插入其他微任务?

今天正好没什么事,就顺着这个点把 Promise 的底层流程重新理了一遍。

先说结论

Promise 最核心的东西可以压缩成几句话:

text
Promise 保存一个异步结果的状态。
then 注册当前 Promise 完成后的 reaction。
then 会返回一个新的 Promise,也就是下一段链式调用要等待的 Promise。
当前 then 回调的返回值,会决定这个新 Promise 的状态。
then 回调不会同步执行,而是通过微任务队列执行。

Promise.all 的核心也不神秘:

text
把传入的每一项都用 Promise.resolve 包一层
给每个 Promise 注册 then
每成功一个就按原始 index 保存结果
成功数量等于总数量时 resolve 总 Promise
任意一个失败时 reject 总 Promise

所以 Promise 不是魔法。

它主要是在做三件事:

text
保存状态
保存回调
用微任务队列按规则执行回调

Promise.all 接收什么

Promise.all 最常见的写法是传一个 Promise 数组:

ts
const result = await Promise.all([
  fetchUser(),
  fetchArticles(),
  fetchProjects(),
]);

但更准确地说,它接收的是一个 Iterable

也就是可以被 for...of 遍历的对象。

数组当然可以:

ts
await Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
]);

Set 也可以:

ts
await Promise.all(
  new Set([
    Promise.resolve(1),
    Promise.resolve(2),
  ]),
);

甚至普通值也可以混进去:

ts
const result = await Promise.all([
  Promise.resolve(1),
  2,
  "hello",
]);

console.log(result);

结果是:

ts
[1, 2, "hello"]

普通值会被当成已经成功的 Promise。

Promise.all 为什么能保持顺序

Promise.all 并不是谁先完成谁排前面。

它返回结果的顺序永远和传入顺序一致。

例如:

ts
const slow = new Promise((resolve) => {
  setTimeout(() => resolve("slow"), 1000);
});

const fast = new Promise((resolve) => {
  setTimeout(() => resolve("fast"), 100);
});

const result = await Promise.all([slow, fast]);

console.log(result);

结果是:

ts
["slow", "fast"]

虽然 fast 先完成,但它传入时在第二个位置,所以结果也在第二个位置。

可以用一个简化版实现理解:

ts
function myPromiseAll<T>(
  items: Iterable<T | Promise<T>>,
): Promise<T[]> {
  return new Promise((resolve, reject) => {
    const arr = Array.from(items);
    const results: T[] = [];
    let completed = 0;

    if (arr.length === 0) {
      resolve([]);
      return;
    }

    arr.forEach((item, index) => {
      Promise.resolve(item)
        .then((value) => {
          results[index] = value;
          completed += 1;

          if (completed === arr.length) {
            resolve(results);
          }
        })
        .catch((error) => {
          reject(error);
        });
    });
  });
}

重点是这一句:

ts
results[index] = value;

每个 Promise 完成时,都把结果放回自己原来的位置。

所以结果顺序稳定。

为什么要 Promise.resolve(item)

手写 Promise.all 时会看到这句:

ts
Promise.resolve(item)

它的作用是把任何输入统一变成 Promise。

如果 item 是普通值:

ts
Promise.resolve(123);

它会变成一个已经 fulfilled 的 Promise。

如果 item 本来就是 Promise:

ts
const p = fetch("/api/user");

Promise.resolve(p);

它会跟随这个 Promise。

如果 item 是 thenable,也就是有 .then() 方法的对象:

ts
const thenable = {
  then(resolve: (value: string) => void) {
    resolve("done");
  },
};

Promise.resolve(thenable).then((value) => {
  console.log(value);
});

它也会被当成 Promise 来处理。

所以:

ts
Promise.resolve(item).then(...)

比直接:

ts
item.then(...)

稳得多。

因为普通值没有 .then(),直接调会报错。

Promise.resolve 相当于把输入标准化:

text
普通值 -> 已成功的 Promise
Promise -> 跟随它的状态
thenable -> 转成真正的 Promise

then 是 Promise 最核心的方法

Promise 最关键的接口是 then

它做的事情大概有三件:

text
注册当前 Promise 成功或失败后的回调
创建并返回一个新的 Promise
用回调的执行结果决定这个新 Promise 的状态

例如:

ts
const p1 = Promise.resolve(1);

const p2 = p1.then((value) => {
  return value + 1;
});

这里的 p2 就是 then 返回的新 Promise。

我们可以把它叫做 nextPromise

它代表:

text
当前 then 回调执行之后的结果。

所以链式调用并不是同一个 Promise 一直往后挂。

而是每次 then 都返回一个新的 Promise:

ts
const p1 = Promise.resolve(1);

const p2 = p1.then((value) => value + 1);

const p3 = p2.then((value) => value * 2);

const p4 = p3.then((value) => {
  console.log(value);
});

关系是:

text
p1 成功 -> 执行第一个 then -> resolve p2
p2 成功 -> 执行第二个 then -> resolve p3
p3 成功 -> 执行第三个 then -> resolve p4

nextPromise 到底是什么

nextPromise 不是规范里的正式名字。

在 ECMAScript 规范里,它更接近:

text
resultCapability.[[Promise]]

可以理解成:

ts
const resultCapability = {
  promise: nextPromise,
  resolve: nextPromiseResolve,
  reject: nextPromiseReject,
};

then 内部大概会做:

ts
Promise.prototype.then = function (onFulfilled, onRejected) {
  const promise = this;

  const resultCapability = NewPromiseCapability();

  PerformPromiseThen(
    promise,
    onFulfilled,
    onRejected,
    resultCapability,
  );

  return resultCapability.promise;
};

这里返回的:

ts
resultCapability.promise

就是 nextPromise

为什么不只创建 Promise,还要把 resolvereject 一起保存?

因为当前 then 的回调将来执行完以后,需要用它的结果来改变 nextPromise 的状态。

例如:

ts
const p2 = p1.then((value) => {
  return value + 1;
});

内部执行当前 reaction 时,大概是:

ts
queueMicrotask(() => {
  try {
    const result = onFulfilled(p1.value);

    resultCapability.resolve(result);
  } catch (error) {
    resultCapability.reject(error);
  }
});

如果回调返回普通值:

ts
return 2;

那么 p2 成功,值是 2

如果回调抛错:

ts
throw new Error("boom");

那么 p2 失败,原因是这个错误。

如果回调返回另一个 Promise:

ts
return fetch("/api/user");

那么 p2 会跟随这个 Promise。

这就是 nextPromise 的作用:

text
它接住当前 then 回调的执行结果,
并把这个结果交给链条里的下一步。

then 不会找错回调

我一开始容易疑惑:

ts
resultCapability.resolve(result);

它怎么知道这个 result 是哪个 onFulfilled 返回的?

不会把别的 then 搞混吗?

答案是不会。

因为每一次 then 都会创建一条 reaction 记录。

这条 reaction 里面同时保存两样东西:

ts
const reaction = {
  handler: onFulfilled,
  capability: resultCapability,
};

也就是说:

text
这个 then 的回调
和
这个 then 返回的新 Promise 的 resolve/reject
是一对一绑在一起的。

比如:

ts
const p2 = p1.then((value) => value + 1);

内部会创建:

ts
const reaction = {
  handler: (value: number) => value + 1,
  capability: {
    promise: p2,
    resolve: p2Resolve,
    reject: p2Reject,
  },
};

如果 p1 还是 pending,这条 reaction 就会被存到 p1 上:

ts
p1.fulfillReactions.push(reaction);

p1 fulfilled 后,才会执行:

ts
for (const reaction of p1.fulfillReactions) {
  enqueueMicrotask(() => {
    const result = reaction.handler(p1.value);
    reaction.capability.resolve(result);
  });
}

所以不会找错。

因为 handler 和 capability 本来就在同一个 reaction 对象里。

多个 then 挂在同一个 Promise 上

还有一种情况:

ts
const p1 = Promise.resolve(1);

const p2 = p1.then((value) => value + 1);
const p3 = p1.then((value) => value * 10);

这里不是 p2 后面接 p3

而是 p2p3 都挂在 p1 上。

内部关系是:

text
p1 上的 reaction A:
handler: value => value + 1
capability: p2

p1 上的 reaction B:
handler: value => value * 10
capability: p3

p1 成功值是 1

text
reaction A 执行 -> result = 2  -> resolve p2
reaction B 执行 -> result = 10 -> resolve p3

它们互不影响。

这也说明,Promise 链和多个订阅不是一回事。

链式调用是:

ts
p1.then(...).then(...).then(...);

多个订阅是:

ts
p1.then(...);
p1.then(...);
p1.then(...);

then 回调为什么不是同步执行

看这个例子:

ts
console.log("1");

Promise.resolve("A").then((value) => {
  console.log("2", value);
});

console.log("3");

输出是:

text
1
3
2 A

原因是:

ts
Promise.resolve("A")

会立刻创建一个已经 fulfilled 的 Promise。

但是:

ts
then(...)

里面的回调不会立刻执行。

它会进入微任务队列。

所以同步代码先执行完:

text
console.log("1")
console.log("3")

然后才执行微任务:

text
console.log("2 A")

可以记成:

text
then 这个函数调用是同步的。
then 里面传入的回调是异步微任务。

已经成功和还在等待的 Promise 有什么区别

如果 Promise 已经 fulfilled:

ts
const p = Promise.resolve("A");

p.then(() => {
  console.log("then");
});

调用 then 时,它会立刻把回调排进微任务队列。

如果 Promise 还在 pending:

ts
const p = new Promise((resolve) => {
  setTimeout(() => {
    resolve("A");
  }, 1000);
});

p.then(() => {
  console.log("then");
});

调用 then 时,回调不会进入微任务队列。

它会先被存在 Promise 里面。

等以后调用:

ts
resolve("A");

Promise 状态变成 fulfilled,才会把之前保存的回调排进微任务队列。

所以 then 内部大概有一个判断:

ts
if (promise.state === "fulfilled") {
  enqueueMicrotask(fulfillReaction);
} else if (promise.state === "rejected") {
  enqueueMicrotask(rejectReaction);
} else {
  promise.fulfillReactions.push(fulfillReaction);
  promise.rejectReactions.push(rejectReaction);
}

这就是我今天比较关键的一个理解点:

text
已经兑现,就马上排微任务。
还在等待,就先存 reaction。
未来 resolve/reject 时,再排微任务。

resolve 一个 Promise 会发生什么

如果这样写:

ts
const inner = Promise.resolve("done");

const outer = new Promise((resolve) => {
  resolve(inner);
});

outer.then((value) => {
  console.log(value);
});

outer 不会成功成 inner 这个对象。

它会跟随 inner 的状态。

所以最后输出的是:

text
done

如果 inner 失败,outer 也会失败。

这叫 Promise 的 resolution procedure。

也就是:

text
如果 resolve 的值是普通值,当前 Promise 用这个普通值 fulfilled。
如果 resolve 的值是 Promise,当前 Promise 采用那个 Promise 的最终状态。
如果 resolve 的值是 thenable,也会尝试跟随它。

所以:

ts
Promise.resolve(Promise.resolve(1))

最后不是:

ts
Promise<Promise<number>>

而是:

ts
Promise<number>

Promise 会自动拆一层。

resolve Promise 时会多等一轮微任务吗

这里有一个比较绕的点。

如果直接 resolve 普通值:

ts
const p = new Promise((resolve) => {
  resolve("x");
});

p.then(() => {
  console.log("p");
});

Promise.resolve().then(() => {
  console.log("microtask");
});

通常会输出:

text
p
microtask

但如果 resolve 的是另一个 Promise:

ts
const inner = Promise.resolve("x");

const outer = new Promise((resolve) => {
  resolve(inner);
});

outer.then(() => {
  console.log("outer");
});

Promise.resolve().then(() => {
  console.log("microtask");
});

通常会输出:

text
microtask
outer

原因是:

ts
resolve(inner);

不是直接让 outer fulfilled。

它需要先进入“采用 inner 状态”的流程。

这个流程本身会经过微任务调度。

所以 outer.then(...) 的回调往往会比直接 resolve 普通值更晚进入微任务队列。

我以前听过一种说法:

text
resolve 一个 Promise 会等两次事件循环。

现在看,这种说法不太精确。

更准确应该是:

text
它可能多等一轮或几轮微任务,
但不是多等一个完整的宏任务事件循环。

Promise 链不是同步递归执行

看这个链:

ts
const p1 = Promise.resolve(1);

const p2 = p1.then((value) => {
  return value + 1;
});

const p3 = p2.then((value) => {
  return value * 2;
});

可以想象成:

text
p1 上有 reactionA:
handler: value => value + 1
capability: p2

p2 上有 reactionB:
handler: value => value * 2
capability: p3

执行时不是:

text
同步执行 reactionA
同步 resolve p2
同步执行 reactionB
同步 resolve p3

而是:

text
微任务 1:
执行 p1 的 reactionA
result = 2
调用 p2Resolve(2)
p2 状态变 fulfilled
把 p2 上的 reactionB 放进微任务队列

微任务 2:
执行 p2 的 reactionB
result = 4
调用 p3Resolve(4)

也就是说:

text
resolve nextPromise 会推动下一步,
但下一步仍然是通过微任务队列执行。

它不是同步递归一路跑到底。

Promise 链中间会夹杂其他微任务吗

会。

因为微任务队列是先进先出。

看这个例子:

ts
const p1 = Promise.resolve(1);

const p2 = p1.then((value) => {
  console.log("A");
  return value + 1;
});

p2.then((value) => {
  console.log("B", value);
});

Promise.resolve().then(() => {
  console.log("C");
});

输出通常是:

text
A
C
B 2

原因是初始微任务队列大概是:

text
A reaction
C

执行 A reaction 时,p2 被 resolve。

然后 p2 上的 B reaction 被放进微任务队列尾部。

这时队列变成:

text
C
B reaction

所以先执行 C,再执行 B

这说明:

text
Promise 链不是一个不可打断的连续同步过程。
每一步 then 都是微任务。
中间可能夹进其他已经排队的微任务。

catch 和 finally 也可以理解成 then 的变体

catch 大致等价于:

ts
promise.then(undefined, onRejected);

所以:

ts
fetch("/api/user")
  .then((response) => response.json())
  .catch((error) => {
    console.error(error);
  });

其实也是一条 Promise 链。

finally 则是不关心成功值或失败原因,只是在结束时执行一段逻辑。

它也会返回一个新的 Promise。

所以 Promise 的核心仍然是:

text
then 创建 nextPromise
reaction 执行后 resolve/reject nextPromise

回到项目里的 Promise.all

再回头看项目里的代码:

ts
const [articles, projects, hobbies, contentCounts] = await Promise.all([
  getPublishedArticles(),
  getPublishedProjects(),
  getPublishedHobbies(),
  getPublishedContentCounts(),
]);

现在就能更清楚地理解它:

text
getPublishedArticles() 先被调用,返回 Promise
getPublishedProjects() 先被调用,返回 Promise
getPublishedHobbies() 先被调用,返回 Promise
getPublishedContentCounts() 先被调用,返回 Promise

Promise.all 给每一项注册 then
谁先完成都可以
完成后按原 index 存结果
四个都完成后,Promise.all 返回的总 Promise fulfilled
await 拿到结果数组
数组按顺序解构成 articles、projects、hobbies、contentCounts

它不是让四个任务排队执行。

而是四个任务都先启动。

Promise.all 负责等待它们全部完成。

如果其中一个抛错,并且没有在内部捕获,Promise.all 返回的总 Promise 就会 reject。

不过我这个项目里的读取函数内部基本都有 try/catch 和 fallback。

所以即使数据库暂时不可用,函数也会返回默认内容,不容易让首页直接崩掉。

我最后想通的地方

今天这次学习最大的收获,不是记住了几个 API。

而是把 Promise 链条背后的关系理顺了。

以前我看:

ts
promise
  .then(step1)
  .then(step2)
  .then(step3);

会下意识觉得这是一个 Promise 后面接了很多函数。

现在更准确的理解是:

text
每个 then 都创建一个新的 Promise。
每个 then 都创建一条 reaction。
reaction 里保存当前回调和 nextPromise 的 resolve/reject。
当前 Promise 完成后,执行自己的 reactions。
回调结果会 resolve/reject nextPromise。
nextPromise 完成后,再触发它自己的 reactions。

也就是:

text
p1 -> reactionA -> p2 -> reactionB -> p3 -> reactionC -> p4

而不是:

text
一个 Promise 上同步执行一串函数。

另一个想通的点是微任务。

then 回调不会同步执行。

已经 fulfilled 的 Promise,调用 then 时会把回调排进微任务队列。

pending 的 Promise,调用 then 时会先保存 reaction,等以后 resolve/reject 再排微任务。

Promise 链的每一步也是通过微任务往后推进的,所以中间可能夹杂其他已经排队的微任务。

这样再看 Promise.all,就清楚很多:

text
它不是并发魔法。
它只是统一包装每一项,
给每一项注册 then,
按 index 收集结果,
最后 resolve 自己返回的那个总 Promise。

这次是从项目里一行 Promise.all 顺藤摸瓜学下去。

感觉这种学习方式挺舒服的。

不是为了背 API 而学,而是在真实代码里遇到一个点,然后往底层多挖一点。

等下次再看到 Promise 链、awaitPromise.allPromise.resolve 的时候,脑子里就不只是“它能用”,而是能大概知道它内部是怎么一步一步推进的。