Skip to content

Promise

Promise 的 .then() 回调之所以先于 setTimeout 执行,不是因为“Promise 更快”,而是因为它们进入的是不同的调度队列:microtask queue 和 macrotask queue。理解这一区分,比单独记忆 Promise API 更关键。


事件循环中的 Promise:microtask 的调度机制

JS 引擎的事件循环有两条队列:

text
Macrotask Queue (Task Queue)
  ├── setTimeout / setInterval
  ├── I/O
  ├── UI rendering
  └── postMessage / MessageChannel

Microtask Queue
  ├── Promise.then / .catch / .finally
  ├── queueMicrotask
  ├── MutationObserver
  └── async/await(本质上 await 之后的代码 = .then())

关键规则:每个 macrotask 执行完毕后,在进入下一个 macrotask 之前,必须清空 microtask queue。

这个规则直接决定了以下代码的执行顺序:

js
console.log('1');

setTimeout(() => console.log('2'), 0);

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

console.log('4');

// 输出:1 → 4 → 3 → 2
// 解释:
// 1: 同步代码
// 4: 同步代码
// → 当前 macrotask 结束,开始清空 microtask queue
// 3: Promise.then() 微任务
// → microtask queue 清空,进入下一个 macrotask
// 2: setTimeout 回调

queueMicrotask API 是 Promise 内部用来调度 .then() 回调的底层机制。在 V8 中,Promise.prototype.then 的 C++ 实现调用 EnqueueMicrotask,将回调注册到当前执行上下文的微任务队列中。

为什么不用 setTimeout(fn, 0) 来实现异步?因为那会把回调放入 macrotask queue,排在当前队列中所有已有的 macrotask 之后,且在每个 macrotask 之间浏览器可能插入渲染帧——延迟不可控。Microtask 在当前 macrotask 结束后的第一时间执行,且"一口气"清空整个队列,中间不插入渲染。

这种"一口气清空"的行为也是 Promise 的一个潜在风险:如果你在一个 .then() 回调中递归地创建新的 Promise 并链式调用,microtask queue 会不断增长,导致主线程被 microtask 持续占用,浏览器无法插入渲染帧——页面看起来就像卡死了。

js
// ⚠️ microtask 风暴:浏览器无法渲染
function microtaskStorm() {
  Promise.resolve().then(() => {
    doSomeWork();
    microtaskStorm(); // 递归注册微任务 → microtask queue 永远不会空
  });
}

Promise/A+ 规范中的关键边界条件

手写 Promise 如果不读规范,很容易实现出"看起来对但不完全对"的行为。Promise/A+ 规范中有几个容易出错的点:

1. Promise Resolution Procedure(§2.3)

.then()onFulfilled 返回一个值 x 时,规范定义了 [[Resolve]](promise, x) 过程:

  • 如果 xpromise 是同一个对象 → reject TypeError(防止循环引用)
  • 如果 x 是一个 Promise → 采用 x 的状态和值
  • 如果 x 是一个对象或函数(且不为 null),且具有 then 方法 → 将其视为 thenable,调用 x.then(resolve, reject)
  • 否则 → 用 x resolve promise

这个 thenable 处理是 Promise 实现中最容易遗漏的细节。它允许不同 Promise 库之间互操作——一个库的 Promise 可以 .then() 另一个库的 thenable 对象。

2. 值的穿透(Value Penetration)

.then()onFulfilled 不是函数时,规范要求"沿链向下传递值":

js
Promise.resolve(42)
  .then(null)        // onFulfilled 不是函数 → 值穿透
  .then(undefined)   // 同上
  .then(v => console.log(v)); // 输出 42

这不是"跳过 null",而是规范明确规定的行为:如果 onFulfilled 不是函数,则用当前值 resolve 新 Promise。

3. 错误穿透

.then()onRejected 不是函数时,错误沿链向下传播:

js
Promise.reject('error')
  .then(v => v)            // onRejected 不是函数 → 错误穿透
  .then(v => v)            // 同上
  .catch(e => console.log(e)); // 输出 'error'

4. thenable 的鸭子类型检测

规范用"是否存在 .then 方法"来判断一个对象是否是 Promise-like,而不是用 instanceof Promise。这意味着任何带 .then() 方法的对象都可以被 Promise 系统消费:

js
const thenable = {
  then(resolve, reject) {
    setTimeout(() => resolve('ok'), 1000);
  }
};

Promise.resolve(thenable).then(v => console.log(v)); // 1 秒后输出 'ok'

这个设计选择是故意的——它让 ES6 Promise 和之前存在的 Promise 库(Bluebird、Q、jQuery Deferred)能够互操作。代价是任何恰好有 .then 属性的对象可能被误认为是 Promise,但实际工程中极少遇到这种冲突。


手写 Promise 实现(TypeScript 完整版)

以下实现覆盖了规范的核心要求:状态不可逆、queueMicrotask 调度、Resolution Procedure、thenable 检测、值穿透、完整的组合子。

ts
type Resolve<T> = (value: T | PromiseLike<T>) => void;
type Reject = (reason?: any) => void;
type Executor<T> = (resolve: Resolve<T>, reject: Reject) => void;
type OnFulfilled<T, R> = ((value: T) => R | PromiseLike<R>) | undefined | null;
type OnRejected<R> = ((reason: any) => R | PromiseLike<R>) | undefined | null;

enum State { PENDING, FULFILLED, REJECTED }

interface PromiseLike<T> {
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
    onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
  ): PromiseLike<TResult1 | TResult2>;
}

class MyPromise<T> {
  private state: State = State.PENDING;
  private value!: T;
  private reason!: any;
  private onFulfilledCallbacks: Array<() => void> = [];
  private onRejectedCallbacks: Array<() => void> = [];

  constructor(executor: Executor<T>) {
    const resolve: Resolve<T> = (value) => {
      if (this.state !== State.PENDING) return;
      // Resolution Procedure: 如果 resolve 的值是一个 thenable,需要等待它
      if (value instanceof MyPromise || (value !== null && typeof value === 'object' && typeof (value as any).then === 'function')) {
        (value as PromiseLike<T>).then(resolve, reject);
        return;
      }
      this.state = State.FULFILLED;
      this.value = value as T;
      // 规范要求 .then() 回调异步执行 → queueMicrotask
      this.onFulfilledCallbacks.forEach(cb => queueMicrotask(cb));
    };

    const reject: Reject = (reason) => {
      if (this.state !== State.PENDING) return;
      this.state = State.REJECTED;
      this.reason = reason;
      this.onRejectedCallbacks.forEach(cb => queueMicrotask(cb));
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then<TResult1 = T, TResult2 = never>(
    onFulfilled?: OnFulfilled<T, TResult1>,
    onRejected?: OnRejected<TResult2>
  ): MyPromise<TResult1 | TResult2> {
    return new MyPromise<TResult1 | TResult2>((resolve, reject) => {
      const handleFulfilled = () => {
        try {
          if (typeof onFulfilled === 'function') {
            const result = onFulfilled(this.value);
            result instanceof MyPromise
              ? result.then(resolve as any, reject)
              : resolve(result as TResult1);
          } else {
            // 值穿透: onFulfilled 不是函数时,用当前值 resolve
            resolve(this.value as unknown as TResult1);
          }
        } catch (e) {
          reject(e);
        }
      };

      const handleRejected = () => {
        try {
          if (typeof onRejected === 'function') {
            const result = onRejected(this.reason);
            result instanceof MyPromise
              ? result.then(resolve as any, reject)
              : resolve(result as TResult1);
          } else {
            // 错误穿透: onRejected 不是函数时,继续向下传播错误
            reject(this.reason);
          }
        } catch (e) {
          reject(e);
        }
      };

      if (this.state === State.FULFILLED) {
        queueMicrotask(handleFulfilled);
      } else if (this.state === State.REJECTED) {
        queueMicrotask(handleRejected);
      } else {
        this.onFulfilledCallbacks.push(handleFulfilled);
        this.onRejectedCallbacks.push(handleRejected);
      }
    });
  }

  catch<TResult = never>(
    onRejected?: OnRejected<TResult>
  ): MyPromise<T | TResult> {
    return this.then(undefined, onRejected);
  }

  finally(onFinally?: (() => void) | undefined | null): MyPromise<T> {
    return this.then(
      // 规范: 如果 onFinally 返回一个 Promise,等它完成后再继续
      value => {
        if (typeof onFinally === 'function') {
          const result = onFinally();
          if (result instanceof MyPromise) {
            return result.then(() => value);
          }
        }
        return value;
      },
      reason => {
        if (typeof onFinally === 'function') {
          const result = onFinally();
          if (result instanceof MyPromise) {
            return result.then(() => { throw reason; });
          }
        }
        throw reason;
      }
    );
  }

  // --- 静态方法 ---

  static resolve<U>(value?: U | PromiseLike<U>): MyPromise<U> {
    if (value instanceof MyPromise) return value;
    return new MyPromise<U>(resolve => resolve(value as U));
  }

  static reject<U = never>(reason?: any): MyPromise<U> {
    return new MyPromise<U>((_, reject) => reject(reason));
  }

  static all<T extends readonly unknown[] | []>(
    promises: T
  ): MyPromise<{ -readonly [P in keyof T]: Awaited<T[P]> }> {
    return new MyPromise((resolve, reject) => {
      if (promises.length === 0) return resolve([] as any);
      const results: any[] = new Array(promises.length);
      let completed = 0;
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          value => {
            results[index] = value;
            completed++;
            if (completed === promises.length) resolve(results as any);
          },
          reject // 任一个 reject → 立即 reject
        );
      });
    });
  }

  static race<T extends readonly unknown[] | []>(
    promises: T
  ): MyPromise<T[number]> {
    return new MyPromise((resolve, reject) => {
      promises.forEach(promise => {
        MyPromise.resolve(promise).then(resolve, reject);
      });
    });
  }

  static allSettled<T extends readonly unknown[] | []>(
    promises: T
  ): MyPromise<{ -readonly [P in keyof T]: PromiseSettledResult<Awaited<T[P]>> }> {
    return new MyPromise(resolve => {
      if (promises.length === 0) return resolve([] as any);
      const results: any[] = new Array(promises.length);
      let completed = 0;
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          value => {
            results[index] = { status: 'fulfilled', value };
            completed++;
            if (completed === promises.length) resolve(results as any);
          },
          reason => {
            results[index] = { status: 'rejected', reason };
            completed++;
            if (completed === promises.length) resolve(results as any);
          }
        );
      });
    });
  }

  static any<T extends readonly unknown[] | []>(
    promises: T
  ): MyPromise<T[number]> {
    return new MyPromise((resolve, reject) => {
      if (promises.length === 0) return reject(new AggregateError([], 'All promises were rejected'));
      const errors: any[] = new Array(promises.length);
      let failed = 0;
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          resolve, // 任意一个成功 → resolve
          reason => {
            errors[index] = reason;
            failed++;
            if (failed === promises.length) {
              reject(new AggregateError(errors, 'All promises were rejected'));
            }
          }
        );
      });
    });
  }

  static withResolvers<T>(): {
    promise: MyPromise<T>;
    resolve: Resolve<T>;
    reject: Reject;
  } {
    let resolve!: Resolve<T>, reject!: Reject;
    const promise = new MyPromise<T>((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  }
}

几个实现要点:

  • Resolution Procedure 在 resolve 中处理:如果 resolve 接收到一个 thenable,必须等待它 resolve。这处理了 resolve(new MyPromise(...)) 这种情况。
  • queueMicrotask 用于延迟执行:已 resolve/reject 的 Promise 的 .then() 回调必须异步执行。不能用 setTimeout(见上文 microtask vs macrotask)。
  • finally 的回调如果返回 Promise 需要等待:规范要求 finally(fn) 中如果 fn() 返回一个 Promise,后续的 .then() 要等该 Promise 完成。
  • all 的空数组直接 resolvePromise.all([]) 返回 [],这是规范规定的边界行为。
  • any 的空数组 reject AggregateError:与 all 相反。

Promise 组合子:选择正确的工具

六个组合子各有明确的适用边界。组合子选择错误时,不只是代码风格问题,也可能直接引入逻辑错误:

组合子语义使用场景失败条件
all全部成功才成功并行请求,缺一不可任一 reject
allSettled全部完成后报告批量操作,每个独立结果都要永不 reject
race最快的获胜超时控制、竞速最快的 reject 则 reject
any任意一个成功就好多副本请求(CDN fallback)全部 reject 才 reject
withResolvers外部控制 resolvePromise + 回调桥接由使用者控制
try(ES2024 提案)包装任意函数返回值不确定 fn 是同步还是异步fn 抛出则 reject

race 的一个生产坑:如果快速 reject 的那个 Promise 不是你关心的,race 仍然会因为最快的失败而失败:

js
const data = fetch('/api/data');          // 200ms
const timeout = new Promise((_, reject) =>
  setTimeout(() => reject('timeout'), 100) // 100ms → 先 reject
);

await Promise.race([data, timeout]); // → 'timeout',data 请求还在进行中!

race 只关心第一个 settled 的结果,但它不会取消慢的那个 Promise。对于超时控制,更好的做法是用 AbortController

js
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
  const res = await fetch('/api/data', { signal: controller.signal });
} finally {
  clearTimeout(timeout);
}

async/await 与原始 Promise 的本质差异

async/await 不是 Promise.then() 的语法糖。两者在三个维度上有本质差异:

1. 执行暂停语义

js
// Promise 链:每次 .then() 后立即继续,不暂停外部代码
function chain() {
  fetchData()
    .then(a => processA(a))
    .then(b => processB(b));
  console.log('继续执行'); // ← 立即输出
}

// async/await:await 之后的代码被挂起,外部代码继续
async function asyncFn() {
  const a = await fetchData();    // ← 这里暂停 asyncFn 的执行
  const b = await processA(a);    // ← 但主线程继续
  processB(b);
}

await 将函数执行"拆分"为多个 microtask:await 之前的代码同步执行,await 之后的代码被注册为 microtask。这与 Generator 函数的 yield 行为类似——事实上 async/await 在规范层面就是 Generator + Promise 的组合。

2. 错误传播

js
// Promise 链:错误沿链向下传播到最近的 .catch()
fetchData()
  .then(a => { throw new Error('fail'); })
  .then(b => console.log('不会执行'))
  .catch(e => console.error('捕获到:', e));

// async/await:错误可以用 try-catch 捕获
async function fn() {
  try {
    const a = await fetchData();
    throw new Error('fail');
    const b = await processA(a); // 不会执行
  } catch (e) {
    console.error('捕获到:', e);
  }
}

try-catch 可以跨多个 await 统一捕获错误,比 .catch() 链在视觉上更清晰。但要注意:await 表达式后面的代码可能在 try 块中但错误可能来自更早的 await——这恰好是期望行为。

3. 并发表达力

js
// ❌ 串行——两个请求不需要依赖但被串行执行
const a = await fetchA();
const b = await fetchB(); // 等 a 完成后才开始

// ✅ 并行——用 Promise.all 同时发起
const [a, b] = await Promise.all([fetchA(), fetchB()]);

// ✅ 并发控制——限制同时进行的请求数
async function asyncPool(limit, tasks, fn) {
  const results = [];
  const executing = new Set();
  for (const task of tasks) {
    const p = Promise.resolve().then(() => fn(task));
    results.push(p);
    executing.add(p);
    const clean = () => executing.delete(p);
    p.then(clean, clean);
    if (executing.size >= limit) {
      await Promise.race(executing); // 等待任意一个完成
    }
  }
  return Promise.all(results);
}

async/await 默认是串行的——每个 await 都要等上一个完成。实现并发需要用 Promise.allPromise.race 或手动管理执行池。这是 async/await 最容易误导初学者的地方:看起来像同步代码,但默认是串行执行。


生产中的 Promise 错误处理

Unhandled Rejection 的检测

Promise 被 reject 但没有 .catch()await 的错误处理时,浏览器触发 unhandledrejection 事件:

js
window.addEventListener('unhandledrejection', event => {
  console.error('未处理的 Promise 拒绝:', event.reason);
  // 上报到监控系统
  analytics.track('unhandled_rejection', {
    message: event.reason?.message,
    stack: event.reason?.stack,
    url: location.href,
  });
  event.preventDefault(); // 阻止默认的控制台错误输出
});

浏览器检测 unhandled rejection 的机制:当一个 Promise 被 reject 时,引擎不立即判断"未处理",而是等待一个 microtask tick。如果在这个 tick 内,有 .catch()then(undefined, onReject) 被注册到这个 Promise,它就不再是 unhandled。过了这个 tick 仍然没有处理,unhandledrejection 事件触发。

这个机制意味着:

js
const p = Promise.reject('error');
// 此时还没有 .catch()
setTimeout(() => {
  p.catch(e => console.log('处理了')); // 在 macrotask 中才注册 → 太晚了
}, 0);
// unhandledrejection 事件已经被触发了

Node.js 中 --unhandled-rejections=strict 标志会将 unhandled rejection 转为进程退出,在生产环境中通常不会开启。更常见的做法是全局监听 unhandledRejection 事件并上报。

生产中的常见 Promise 陷阱

  1. 忘记 return Promise
js
// ❌ 这个 .catch() 捕获不到错误,因为外部没有 return
function fetchWrapper() {
  try {
    fetchData().catch(e => console.error(e)); // catch 了,但没有 return
  } catch (e) {
    // 永远不会执行——Promise 的 reject 不会 throw 到同步代码
  }
}
  1. forEach 中的 async 不会等待
js
// ❌ forEach 中的 async 函数不会被等待
[1, 2, 3].forEach(async (id) => {
  await processItem(id); // forEach 不关心返回值
});
console.log('done'); // ← 在所有 processItem 完成之前就输出了

// ✅ for...of 会正确等待
for (const id of [1, 2, 3]) {
  await processItem(id);
}
  1. Promise 构造函数中的错误被吞掉
js
// ❌ new Promise 构造函数中的同步错误被自动 reject,但不记录
new Promise((resolve, reject) => {
  throw new Error('constructor error'); // → reject,但如果没 .catch() 就是 unhandled
});

// ✅ 构造函数中总是 try-catch 或传入 AbortSignal
new Promise((resolve, reject) => {
  try {
    doSomething();
  } catch (e) {
    reject(e);
  }
});

扩展问题

Q1: Promise.resolve().then(() => {}).then(() => {}) 中有多少个 microtask?它们的调度顺序是怎样的?

两个 microtask。第一个 Promise.resolve() 是已 resolved 状态,但其 .then() 回调不会在当前 tick 的同步代码中执行——规范要求它被调度到 microtask queue。

执行顺序:

  1. 当前同步代码执行完毕
  2. Microtask 1(第一个 .then())被执行
  3. 第一个 .then() 返回一个新的 resolved Promise
  4. Microtask 2(第二个 .then())被加入 microtask queue
  5. Microtask 2 执行

关键点:即使 Promise 已经是 resolved 状态,.then() 回调仍然是异步的。这是在 Promise/A+ 规范 §2.2.4 中明确要求的——"onFulfilled or onRejected must not be called until the execution context stack contains only platform code"。

Q2: Promise.all 如果传入的数组中有非 Promise 值,会发生什么?为什么这样设计?

Promise.all 会用 Promise.resolve() 包裹每个元素。非 Promise 值会被转换为立即 resolved 的 Promise。所以 Promise.all([1, 2, Promise.resolve(3)]) 等价于 Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])

这样设计的意图是:Promise.all 的调用者不需要先确保数组中所有值都是 Promise。你可以传入混合数组,all 自己处理类型统一。这在"我有一个值列表,其中有些可能是异步的"场景下大幅简化了调用方代码。

Q3: Promise.racePromise.any 在"第一个 resolve 后"的行为有什么不同?

race:第一个 settled(无论 fulfilled 还是 rejected)就返回它的结果。如果最快的是 reject,race 也 reject,即使有其他 Promise 后续成功。

any:第一个 fulfilled 就返回它的值。如果全部 reject,返回 AggregateError。any 忽略 reject,只关心第一个成功。

这个差异决定了使用场景:

  • race → 超时控制(timeout 先 reject → 超时生效)
  • any → 多副本请求(哪个 CDN 先返回成功就用哪个,失败的副本不影响结果)

Q4: 在 async 函数中,await 之后的代码是什么时候执行的?如果 await 的对象是一个已经 resolved 的 Promise,行为有区别吗?

await 等价于 .then()——它总是将之后的代码作为 microtask 调度,即使 await 的对象已经是 resolved 状态。没有"同步优化"。

js
async function test() {
  console.log('A');
  await Promise.resolve('ok');
  console.log('B'); // 总是异步执行
}
test();
console.log('C');
// 输出: A → C → B

即使 Promise.resolve('ok') 是已 resolved 的,B 仍然被放到 microtask queue,在当前 macrotask 的同步代码全部执行完之后才输出。

Q5: 手写 Promise 的 then 中,为什么需要判断 result instanceof MyPromise?不能只判断 typeof result === 'object' && typeof result.then === 'function' 吗?

可以,而且规范就是这样要求的(thenable 鸭子类型检测)。用 instanceof MyPromise 是一种优化——对于自己库创建的 Promise,走 instanceof 快速路径;对于其他库的 Promise(thenable),走 .then() 检测路径。

但只用 instanceof 是不够的——如果你的 Promise 和另一个 Promise 库交互(比如你的代码同时用了 Bluebird),Bluebird 的 Promise 不是你的 MyPromise 的实例,但它有 .then() 方法。所以规范的 thenable 检测(typeof x === 'object' && typeof x.then === 'function')是确保跨库互操作性的必要条件。instanceof 只是对自身库的快捷路径。

在完整实现中,两者应该并用:先检查 instanceof(快速自检),不匹配时 fallback 到 thenable 检测(通用互操作)。