手写一个Promises/A+和理解核心概念

377 阅读7分钟

手写一个Promises/A+和理解核心概念

这篇文章我们通过划分功能逐个实现 Promise 的功能,了解这个异步任务处理机制的实现,知道 Promise 实例 then 方法的链式调用原理,梳理 Promise 核心的裁决过程,并顺利通过Promise/A+规范的测试用例库。

一、安装和运行测试用例库

  1. 初始化项目安装 promises-aplus-tests 测试用例库
mkdir myPromise && cd myPromise
npm init -y && npm i promises-aplus-tests -D
  1. 创建一个 index.js 文件并输入用例库使用规则
module.exports = {
  resolved: () => {},
  rejected: () => {},
  deferred: () => {},   
};
  1. 本地跑一下测试用例库
npx promises-aplus-tests index.js
  1. 查看测试结果(871个测试用例全部失败了)

test-result.png

可以看到我们测试失败的原因都是没有定义某些属性或者方法,接下来我们就要实现功能并通过这八百多个测试用例了。

二、基本状态处理和回调执行

这里主要定义 Promise 的三种状态和执行器函数(executor),并处理执行器函数中的回调函数。

class MyPromise {
  static PENDING = 'pending'; // 进行中
  static FULFILLED = 'fulfilled'; // 已成功
  static REJECTED = 'rejected'; // 已失败
  state = MyPromise.PENDING;
  value = null;
  reason = null;
  onFulfilledCallbacks = [];
  onRejectedCallbacks = [];

  constructor(executor) {
    try {
      //实例化时会立即执行
      executor(this.resolve, this.reject);
    } catch (reason) {
      this.reject(reason);
    }
  }

  resolve = (value) => {
    if (this.state === MyPromise.PENDING) {
      //切换为成功状态
      this.state = MyPromise.FULFILLED;
      this.value = value;
      this.onFulfilledCallbacks.forEach((callback) => callback());
    }
  };

  reject = (reason) => {
    if (this.state === MyPromise.PENDING) {
      //切换为失败状态
      this.state = MyPromise.REJECTED;
      this.reason = reason;
      this.onRejectedCallbacks.forEach((callback) => callback());
    }
  };

  then(onFulfilled, onRejected) {
    // 根据状态判断处理两个回调函数
    switch (this.state) {
      case MyPromise.FULFILLED:
        // 处理用户想要的成功回调
        onFulfilled(this.value);
        break;
      case MyPromise.REJECTED:
        // 处理用户想要的失败回调
        onFulfilled(this.value);
        break;
      case MyPromise.PENDING:
        // 进行中的Promise先存储回调
        this.onFulfilledCallbacks.push(() => {
          onFulfilled(this.value);
        });
        this.onRejectedCallbacks.push(() => {
          onRejected(this.reason);
        });
        break;
    }
  }
}

my-promise-base.png

三、完善 then 方法的异步和链式调用

我们知道 Promise 实例的 then 方法的回调函数是晚于同步任务的,因此可以用 setTimeout 来模拟异步,同时因为 then 支持链接调用,所以方法要返回的是一个新的 Promise 实例。

  then(onFulfilled, onRejected) {
    // 添加:如果 onFulfilled 不是函数,其必须被忽略 见规范2.2.1.2
    if (typeof onFulfilled != 'function') {
      onFulfilled = (value) => value;
    }
    // 添加:如果 onRejected 不是函数,其必须被忽略 见规范2.2.1.2
    if (typeof onRejected != 'function') {
      onRejected = (reason) => {
        throw reason;
      };
    }
    return new MyPromise((resolve, reject) => {
      switch (this.state) {
        case MyPromise.FULFILLED:
          // 添加:setTimeout模拟异步
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolve(x);
            } catch (reason) {
              reject(reason);
            }
          }, 0);
          break;
        case MyPromise.REJECTED:
          // 添加:setTimeout模拟异步
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolve(x);
            } catch (reason) {
              reject(reason);
            }
          }, 0);
          break;
        case MyPromise.PENDING:
          this.onFulfilledCallbacks.push(() => {
            // 添加:setTimeout模拟异步
            setTimeout(() => {
              try {
                const x = onFulfilled(this.value);
                resolve(x);
              } catch (reason) {
                reject(reason);
              }
            }, 0);
          });
          this.onRejectedCallbacks.push(() => {
            // 添加:setTimeout模拟异步
            setTimeout(() => {
              try {
                const x = onRejected(this.reason);
                resolve(x);
              } catch (reason) {
                reject(reason);
              }
            }, 0);
          });
          break;
      }
    });
  }

my-promise-then.png

四、核心部分 resolve Promise

上面既然 then 方法返回一个新的 Promise 实例,但 then 方法本身就支持传入一个 Promise 作为回调函数,那么会不会遇到循环调用的问题呢?答案是会的。因此规范提出了一些 Promise 裁决过程,这也是 Promise 最重要的部分,有区分了很多种情况。

  then = (onFulfilled, onRejected) => {
    if (typeof onFulfilled != 'function') {
      onFulfilled = (value) => value;
    }
    if (typeof onRejected != 'function') {
      onRejected = (reason) => {
        throw reason;
      };
    }
    // Promise 核心解决过程 见规范2.3
    const _resolvePromise = (promise, x, resolve, reject) => {
      // 2.3.1 如果 promise 和 x 指向同一对象,避免循环引用,抛出 TypeError 错误
      if (promise === x) {
        const errMsg = 'The promise and the return value are the same';
        return reject(new TypeError(errMsg));
      }

      // 2.3.3 如果 x 为对象(不是null)或者函数
      if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
        let then = null;
        try {
          // 2.3.3.1. 检索属性 x.then
          then = x.then;
        } catch (error) {
          // 2.3.3.2 如果 x.then 导致抛出异常 e,则以 e 为拒绝原因拒绝 promis
          return reject(error);
        }

        // 2.3.3.3 如果 then 是一个函数,x 作为 then 的 this 调用该方法,第一个参数是成功的回调函数,第二个参数是失败的回调函数
        if (typeof then === 'function') {
          let called = false;
          try {
            then.call(
              x,
              (y) => {
                // 2.3.3.3.4 如果成功回调与失败回调都被调用或多次调用同一个参数,则第一个调用优先,其他调用都将被忽略。
                if (called) return;
                called = true;
                // 2.3.3.3.1 如果成功回调以值 y 调用,运行 [[Resolve]](promise,y)
                _resolvePromise(promise, y, resolve, reject);
              },
              (r) => {
                // 2.3.3.3.4 如果成功回调与失败回调都被调用或多次调用同一个参数,则第一个调用优先,其他调用都将被忽略。
                if (called) return;
                called = true;
                // 2.3.3.3.2 如果失败回调以原因 r 调用,用 r 拒绝 promise
                reject(r);
              }
            );
          } catch (error) {
            // 2.3.3.4 如果调用 then 方法抛出异常 e:
            // 2.3.3.4.1 若成功回调或失败回调都调用过,忽略
            if (called) return;

            // 2.3.3.4.2 未调用,用 e 作为原因拒绝 promise
            reject(error);
          }
        } else {
          // 2.3.3.4. 如果 then 不是函数,用 x 作为值完成 promise
          return resolve(x);
        }
      } else {
        // 2.3.4 如果 x 不为对象或者函数,以 x 为参数执行 promise
        return resolve(x);
      }
    };
    // 链式返回的Promise
    const newPromise = new MyPromise((resolve, reject) => {
      switch (this.state) {
        case MyPromise.FULFILLED:
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              _resolvePromise(newPromise, x, resolve, reject);
            } catch (reason) {
              reject(reason);
            }
          }, 0);
          break;
        case MyPromise.REJECTED:
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              _resolvePromise(newPromise, x, resolve, reject);
            } catch (reason) {
              reject(reason);
            }
          }, 0);
          break;
        case MyPromise.PENDING:
          this.onFulfilledCallbacks.push(() => {
            setTimeout(() => {
              try {
                const x = onFulfilled(this.value);
                _resolvePromise(newPromise, x, resolve, reject);
              } catch (reason) {
                reject(reason);
              }
            }, 0);
          });
          this.onRejectedCallbacks.push(() => {
            setTimeout(() => {
              try {
                const x = onRejected(this.reason);
                _resolvePromise(newPromise, x, resolve, reject);
              } catch (reason) {
                reject(reason);
              }
            }, 0);
          });
          break;
      }
    });
    return newPromise;
  };

resolve- promise.png

五、The Promise Resolution Procedure

Promises/A+ 规范中 2.3 The Promise Resolution Procedure 比较复杂,是 Promise 裁决过程,因此这里也提供了一份原文参照。

promise-resolution-procedure.png

六、完整的 Promises/A+ 代码实现

下面是完整的 Promise 手写代码和测试用例库使用。

let promisesAplusTests = require('promises-aplus-tests');

class MyPromise {
  static PENDING = 'pending'; // 进行中
  static FULFILLED = 'fulfilled'; // 已成功
  static REJECTED = 'rejected'; // 已失败
  state = MyPromise.PENDING;
  value = null;
  reason = null;
  onFulfilledCallbacks = [];
  onRejectedCallbacks = [];

  constructor(executor) {
    try {
      //实例化时会立即执行
      executor(this.resolve, this.reject);
    } catch (reason) {
      this.reject(reason);
    }
  }

  resolve = (value) => {
    if (this.state === MyPromise.PENDING) {
      //切换为成功状态
      this.state = MyPromise.FULFILLED;
      this.value = value;
      this.onFulfilledCallbacks.forEach((callback) => callback());
    }
  };

  reject = (reason) => {
    if (this.state === MyPromise.PENDING) {
      //切换为失败状态
      this.state = MyPromise.REJECTED;
      this.reason = reason;
      this.onRejectedCallbacks.forEach((callback) => callback());
    }
  };

  then = (onFulfilled, onRejected) => {
    if (typeof onFulfilled != 'function') {
      onFulfilled = (value) => value;
    }
    if (typeof onRejected != 'function') {
      onRejected = (reason) => {
        throw reason;
      };
    }
    // Promise 核心解决过程 见规范2.3
    const _resolvePromise = (promise, x, resolve, reject) => {
      // 2.3.1 如果 promise 和 x 指向同一对象,抛出 TypeError 错误
      if (promise === x) {
        const errMsg = 'The promise and the return value are the same';
        return reject(new TypeError(errMsg));
      }

      // 2.3.3 如果 x 为对象(不是null)或者函数
      if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
        let then = null;
        try {
          // 2.3.3.1. 检索属性 x.then
          then = x.then;
        } catch (error) {
          // 2.3.3.2 如果 x.then 导致抛出异常 e,则以 e 为拒绝原因拒绝 promis
          return reject(error);
        }

        // 2.3.3.3 如果 then 是一个函数,x 作为 then 的 this 调用该方法,第一个参数是成功的回调函数,第二个参数是失败的回调函数
        if (typeof then === 'function') {
          let called = false;
          try {
            then.call(
              x,
              (y) => {
                // 2.3.3.3.4 如果成功回调与失败回调都被调用或多次调用同一个参数,则第一个调用优先,其他调用都将被忽略。
                if (called) return;
                called = true;
                // 2.3.3.3.1 如果成功回调以值 y 调用,运行 [[Resolve]](promise,y)
                _resolvePromise(promise, y, resolve, reject);
              },
              (r) => {
                // 2.3.3.3.4 如果成功回调与失败回调都被调用或多次调用同一个参数,则第一个调用优先,其他调用都将被忽略。
                if (called) return;
                called = true;
                // 2.3.3.3.2 如果失败回调以原因 r 调用,用 r 拒绝 promise
                reject(r);
              }
            );
          } catch (error) {
            // 2.3.3.4 如果调用 then 方法抛出异常 e:
            // 2.3.3.4.1 若成功回调或失败回调都调用过,忽略
            if (called) return;

            // 2.3.3.4.2 未调用,用 e 作为原因拒绝 promise
            reject(error);
          }
        } else {
          // 2.3.3.4. 如果 then 不是函数,用 x 作为值完成 promise
          return resolve(x);
        }
      } else {
        // 2.3.4 如果 x 不为对象或者函数,以 x 为参数执行 promise
        return resolve(x);
      }
    };
    // 链式返回的Promise
    const newPromise = new MyPromise((resolve, reject) => {
      switch (this.state) {
        case MyPromise.FULFILLED:
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              _resolvePromise(newPromise, x, resolve, reject);
            } catch (reason) {
              reject(reason);
            }
          }, 0);
          break;
        case MyPromise.REJECTED:
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              _resolvePromise(newPromise, x, resolve, reject);
            } catch (reason) {
              reject(reason);
            }
          }, 0);
          break;
        case MyPromise.PENDING:
          this.onFulfilledCallbacks.push(() => {
            setTimeout(() => {
              try {
                const x = onFulfilled(this.value);
                _resolvePromise(newPromise, x, resolve, reject);
              } catch (reason) {
                reject(reason);
              }
            }, 0);
          });
          this.onRejectedCallbacks.push(() => {
            setTimeout(() => {
              try {
                const x = onRejected(this.reason);
                _resolvePromise(newPromise, x, resolve, reject);
              } catch (reason) {
                reject(reason);
              }
            }, 0);
          });
          break;
      }
    });
    return newPromise;
  };
}

/* 测试用例使用规则 */
MyPromise.deferred = function () {
  let result = {};
  result.promise = new MyPromise(function (resolve, reject) {
    result.resolve = resolve;
    result.reject = reject;
  });

  return result;
};

/* 测试用例自定义输入 */
promisesAplusTests(MyPromise, function (err) {
  let { message = 'success' } = err || {};
  console.log('[promises-aplus-tests]', message);
});

module.exports = MyPromise;

七、总结

手写 Promise 并不难,分成几个步骤就比较容易理解了。我们在业务一般都是使用 Promise 实例的 then 方法,那么就能知道 then 方法的传入参数和返回内容。同时因为得到的内容与前面的 Promise 实例化有关,那么就知道该 Promise 在实例化时会保存一些状态和一些回调函数,在适当的时候进行处理。

我们也知道 then 方法是支持多次调用的,他们的状态一旦完成就不会变化,所以使用一个处理成功态的函数数组和处理失败态的函数数组,在链式调用时也是因为 then 回返回一个新的 Promise 实例。

至于最复杂的 resolve Promise 模块,是属于底层逻辑处理了,一般我们不会感知,除了循环调用的情况,例如下面这几行代码。但一般我们不遇到各种负责多变的 Promise 面试题的话,是不用细究实现原理的。

const p = new Promise((resolve, reject) => {
  resolve(1);
});
const newPromise = p.then((data) => {
  return newPromise;
});
// Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>