Promise 与微任务队列

深入理解 Promise 的状态机本质、微任务执行机制,以及 Promise chain 的前因后果与工程实践。


Promise 的本质:状态机

Promise 不是"异步任务本身"。更准确地说:Promise 是 JavaScript 语言层面对异步结果的一个状态机对象

Promise 有三种状态:

  • pending:初始状态,既不是 fulfilled 也不是 rejected
  • fulfilled:操作成功完成,Promise 的值变得可用
  • rejected:操作失败,Promise 带有一个错误原因

状态转换是单向的、不可逆的——一个 Promise 一旦从 pending 变成 fulfilled 或 rejected,就再也不会改变。

Promise 不是任务,而是代理

理解 Promise 的一个关键点是:Promise 不是异步任务本身,而是异步任务结果的代理

真正发起 I/O 操作的,是宿主环境(浏览器或 Node.js)。Promise 只是"承诺"这个 I/O 操作最终会有一个结果,然后提供一种统一的方式来处理这个结果。

// fetch 发起网络请求(真正的 I/O 操作)
const promise = fetch('/api/user');

// Promise 不是网络请求本身
// Promise 是对这个请求结果的代理
promise
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

Promise 的单次决议特性

Promise 是一个单次决议的状态机。一旦 resolved 或 rejected,后续的 .then() 调用会立即执行(作为微任务),而不是重新触发异步操作。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('value'), 1000);
});

promise.then(v => console.log('first then:', v));
promise.then(v => console.log('second then:', v));

// 1 秒后输出:
// first then: value
// second then: value

两个 .then() 都接收到同一个值,因为 Promise resolved 后状态不可改变。


Promise.then() 的本质:注册 Reaction

.then() 的本质不是"取值",而是注册 reaction

Reaction 是一个函数,当 Promise 的状态变为 fulfilled 或 rejected 时被调用。

const promise = Promise.resolve(42);

// .then() 注册了一个 reaction 函数
// 这个函数在 Promise resolved 后作为微任务执行
promise.then(value => {
  console.log('reaction:', value);
});

当多个 .then() 注册在同一个 Promise 上时,它们形成了一个 reaction 链

Promise.resolve(1)
  .then(x => x * 2)      // 注册第一个 reaction
  .then(x => x + 1)      // 注册第二个 reaction
  .then(x => console.log(x)); // 注册第三个 reaction

// 输出: 3 (1*2+1)

Promise Chain 的执行机制

Promise chain 是通过 reaction 连接起来的 future graph。理解它的执行机制,对写出正确的异步代码至关重要。

then 返回新的 Promise

每个 .then() 返回一个新的 Promise。这个新 Promise 的状态取决于 reaction 函数的返回值:

  • 如果 reaction 返回一个值,新 Promise resolved 为这个值
  • 如果 reaction 抛出异常,新 Promise rejected 为这个异常
  • 如果 reaction 返回一个 Promise,新 Promise 会"跟随"那个 Promise
Promise.resolve(1)
  .then(x => {
    console.log('first:', x); // 1
    return x + 1;
  })
  .then(x => {
    console.log('second:', x); // 2
    return Promise.reject('error');
  })
  .then(x => {
    console.log('third:', x); // 不执行
  })
  .catch(err => {
    console.log('catch:', err); // 'error'
  })
  .finally(() => {
    console.log('finally'); // 执行
  });

微任务的时序

Promise reaction 是在微任务队列中执行的。这意味着:

console.log('sync 1');

Promise.resolve()
  .then(() => console.log('microtask'));

console.log('sync 2');

// 输出: sync 1, sync 2, microtask

Promise Resolution Procedure

Promise Resolution Procedure 是 Promise 规范中最复杂的部分之一。它的核心是:Promise 认的是 thenable 协议,是对 thenable 的递归吸收机制

什么是 Thenable

Thenable 是一个普通对象,只要它有 .then() 方法,就是 thenable:

const thenable = {
  then(resolve, reject) {
    resolve(42);
  }
};

// thenable 被 Promise.resolve() 包装后,会被"吸收"
const promise = Promise.resolve(thenable);

promise.then(value => {
  console.log(value); // 42
});

递归吸收机制

Promise.resolve() 如果接收到的值本身是 Promise(或 thenable),它会等待那个 Promise resolved(或 rejected),然后返回一个新的 Promise:

const p1 = new Promise(resolve => setTimeout(() => resolve(1), 100));
const p2 = Promise.resolve(p1); // p2 跟随 p1

p2.then(value => console.log(value)); // 1,100ms 后输出

常见陷阱

陷阱一:忘记了 Promise 是异步的

function getData() {
  const promise = fetch('/api/data');

  return promise; // 正确:返回 Promise 对象
}

async function wrong() {
  const data = getData();
  console.log(data); // Promise 对象,不是数据
}

async function right() {
  const data = await getData();
  console.log(data); // 真实数据
}

陷阱二:忘记 catch promise rejection

// 没有处理 rejection 的 Promise 不会被 GC
// 在 Node.js 中会产生 UnhandledPromiseRejectionWarning
someAsyncOperation()
  .then(result => process(result));

// 正确做法:始终处理 rejection
someAsyncOperation()
  .then(result => process(result))
  .catch(error => console.error(error));

async/await 与 Promise 的关系

async/await 是 Promise 的语法糖,不是新的异步机制。

async 函数的返回值

async 函数总是返回一个 Promise。如果 async 函数正常返回,其 Promise resolved 为返回值;如果抛出异常,Promise rejected 为异常。

async function foo() {
  return 42;
}

foo().then(value => console.log(value)); // 42

await 的等待机制

await 等待的是一个 Promise。await 暂停当前 async 函数的执行,将函数中 await 之后的代码作为微任务加入队列。

async function foo() {
  console.log('a');
  const value = await Promise.resolve(1);
  console.log('b'); // 微任务执行
  return value + 1;
}

Promise 的工程实践

Promise.all:并行等待

const [user, posts, comments] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json())
]);

Promise.all 等待所有 Promise resolved,如果任意一个 rejected,整体立即 rejected。

Promise.race:竞态

// 设置超时
const withTimeout = (promise, timeout) =>
  Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('timeout')), timeout)
    )
  ]);

Promise.allSettled:全部完成

const results = await Promise.allSettled([
  fetch('/api/first').then(r => r.json()),
  fetch('/api/second').then(r => r.json())
]);

results.forEach((result, i) => {
  if (result.status === 'fulfilled') {
    console.log(`API ${i}:`, result.value);
  } else {
    console.log(`API ${i} failed:`, result.reason);
  }
});

这一章想说的

Promise 是 JavaScript 异步编程的核心。它的本质是:

  1. 状态机:pending → fulfilled/rejected,不可逆
  2. 结果代理:不是异步任务本身,而是任务结果的代理
  3. Reaction 注册.then() 注册回调,不是立即执行
  4. 微任务执行:reaction 在微任务队列中执行
  5. 递归吸收:Promise.resolve 遇到 thenable 会递归处理

async/await 是 Promise 的语法糖,理解 Promise 的执行模型,就理解了 async/await。


延展阅读


实践练习

练习一:实现一个 Promise.retry

function fetchWithRetry(url, options = {}, retries = 3) {
  return fetch(url, options)
    .catch(err => {
      if (retries <= 0) throw err;
      return new Promise(resolve =>
        setTimeout(() => resolve(), 1000)
      ).then(() => fetchWithRetry(url, options, retries - 1));
    });
}

思考:这个实现有什么问题?应该如何改进?(提示:考虑什么情况下应该重试,什么情况下不应该)

练习二:分析以下代码的执行顺序

function executor(resolve, reject) {
  console.log('executor');
  resolve();
}

const promise = new Promise(executor);

promise.then(() => console.log('then 1'));
promise.then(() => console.log('then 2'));

console.log('sync');

输出是什么?为什么 then 1then 2 都在 sync 之后输出?