前端知库

怎样手写一个符合 Promises/A+ 规范的 Promise?

核心是三件事:状态只能从 pending 变为 fulfilled 或 rejected 一次;then 的回调总是异步执行,并返回新的 Promise;用 Promise 解决过程处理回调返回值,识别自引用、读取 then 时的异常和 thenable 多次回调。下面的实现通过了 promises-aplus-tests 的全部 872 个用例。

参考答案

简答

  • 状态只能改变一次:resolve、reject 和执行器抛错共用一个 called 标记,先到的生效,之后的调用全部忽略。
  • then 返回新的 Promise。回调不是函数时直接把当前结果传下去(值穿透);回调抛错时新 Promise 拒绝。
  • 回调必须在当前执行栈清空后才运行,即使 Promise 已经完成。这里用 queueMicrotask,与原生 Promise 一样放入微任务队列。
  • 解决过程:值是 Promise 自身时以 TypeError 拒绝;值是对象或函数时只读取一次 then,读取抛错就拒绝;then 是函数时按 thenable 处理,它的两个回调和抛错只认第一次。
  • Promises/A+ 只规定 then 的行为,catch、finally、Promise.resolve 和 Promise.all 都不在规范中。

实现

JavaScript
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
  #state = PENDING;
  #value = undefined;
  #handlers = [];

  constructor(executor) {
    let called = false;
    const resolve = (value) => {
      if (!called) {
        called = true;
        this.#resolve(value);
      }
    };
    const reject = (reason) => {
      if (!called) {
        called = true;
        this.#settle(REJECTED, reason);
      }
    };
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error); // 已经 resolve 过时被 called 挡住
    }
  }

  // Promise 解决过程,对应规范 2.3
  #resolve(x) {
    if (x === this) {
      this.#settle(REJECTED, new TypeError('Chaining cycle detected'));
      return;
    }
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
      let then;
      try {
        then = x.then; // 只读一次,then 可能是有副作用的 getter
      } catch (error) {
        this.#settle(REJECTED, error);
        return;
      }
      if (typeof then === 'function') {
        let called = false; // thenable 可能多次调用回调,只认第一次
        try {
          then.call(
            x,
            (y) => {
              if (!called) {
                called = true;
                this.#resolve(y); // y 仍可能是 thenable,递归解决
              }
            },
            (r) => {
              if (!called) {
                called = true;
                this.#settle(REJECTED, r);
              }
            },
          );
        } catch (error) {
          if (!called) {
            called = true;
            this.#settle(REJECTED, error);
          }
        }
        return;
      }
    }
    this.#settle(FULFILLED, x);
  }

  #settle(state, value) {
    if (this.#state !== PENDING) return;
    this.#state = state;
    this.#value = value;
    this.#handlers.forEach((handler) => this.#run(handler));
    this.#handlers = [];
  }

  #run({ onFulfilled, onRejected, resolve, reject }) {
    queueMicrotask(() => {
      const callback = this.#state === FULFILLED ? onFulfilled : onRejected;
      if (typeof callback !== 'function') {
        // 值穿透:把结果原样交给下一个 Promise
        (this.#state === FULFILLED ? resolve : reject)(this.#value);
        return;
      }
      try {
        resolve(callback(this.#value));
      } catch (error) {
        reject(error);
      }
    });
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const handler = { onFulfilled, onRejected, resolve, reject };
      if (this.#state === PENDING) this.#handlers.push(handler);
      else this.#run(handler);
    });
  }
}

#run 中的 resolve 是新 Promise 构造时拿到的那个,它会再次进入解决过程,所以回调返回另一个 Promise 或 thenable 时,新 Promise 会跟随它的结果。

用规范测试验证

JavaScript
const promisesAplusTests = require('promises-aplus-tests');

const adapter = {
  deferred() {
    let resolve;
    let reject;
    const promise = new MyPromise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  },
};

promisesAplusTests(adapter, (err) => {
  if (err) console.error(err);
});
// 本实现在 Node.js 中运行输出:872 passing

因为实现了标准的 then,原生 Promise.resolve(myPromise) 和 await myPromise 也能把它当作 thenable 使用。

常见追问

  • 为什么不能同步执行已完成 Promise 的回调?那样回调有时同步、有时异步,调用方的代码顺序会随状态变化。规范要求统一在执行栈清空后运行。
  • 怎样补上 catch 和 finally?catch(fn) 就是 then(undefined, fn)。finally 的回调不接收结果,执行后把原结果或原错误继续传下去。
  • 静态方法怎样实现?以 Promise.all 为例,见《怎样实现保持输入顺序的 Promise.all 教学版本?》。