理解&实现(三):Promise

800 阅读6分钟

前言

遥想当年,自学前端(零星学习 6 个月+集中学习 4 个月)后,开始准备面试。在一个交流群里看到一位同学面试让手写 Promise。当时我对 Promise 的理解还仅限于读过阮一峰老师的《ES6 入门教程》,基本没有一点使用经历。无知的我在网上找了一个 Promise 实现,战战兢兢的抄写了一遍,没能理解。一年多以后,又要准备面试了,这一次,这块硬骨头终于啃下来啦,超级开心。😊

接下来让我们一起,理解、实现 Promise 吧。(本文仅包括 Promise 的简单实现,Promise 的基本用法参考阮一峰老师的ES6入门教程。)

三种状态

  1. Promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。一旦状态改变,就不会再变。
  2. Promise 是一个构造函数,接受一个函数 executor 作为参数。
  3. 函数 executor 的两个参数分别是 resolve 和 reject。。
  4. resolve 和 reject 都是函数,改变 Promise 的状态,执行then 方法注册的回调
    • resolve 将状态从 pending 变为 resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
    • reject 将状态从 pending 变为 rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
  5. Promise 实例化时,函数 executor 函数会立即执行,如果执行出错,reject(error)。
const PENDING = "Pending";
const FULFILLED = "Fulfilled";
const REJECTED = "Rejected";

class MyPromise {
  status = PENDING;
  value;
  reason;
  successCallbacks = [];
  failCallbacks = [];
  constructor(executor) {
    if (typeof executor !== "function")
      throw new TypeError("Promise resolver is not a function at new Promise");
    try {
      executor(this.resolve, this.reject);
    } catch (error) {
      this.reject(error);
    }
  };

  resolve = (value) => {
    if (this.status !== PENDING) return;
    this.status = FULFILLED;
    this.value = value;
    this.successCallbacks.forEach((cb) => cb());
  };

  reject = (reason) => {
    if (this.status !== PENDING) return;
    this.status = REJECTED;
    this.reason = reason;
    this.failCallbacks.forEach((cb) => cb());
  };
}

实例方法

Promise.prototype.then()

  1. then 方法接受两个参数,分别是 successCallback 和 failCallback。根据当前状态执行相应的回调。
  2. successCallback 和 failCallback 这两个参数为可选参数,默认值分别为 v=>ve=>{throw e},将值或失败理由向后传递;两个回调参数如果执行出错,reject(error)。
  3. then 方法调用时,返回一个新的 promise。
    • 如果状态已经改变(fufilled 或 rejected),执行状态对应的回调。
    • 如果状态没有改变,还是 pending(异步操作后才改变状态),需将回调保存,在 resolve 或 reject 执行改变状态后再执行回调。
  4. 同一个 promise 对象的 then 方法,可以多次调用。
  5. then 方法可链式调用,下一个 then 方法拿到的是当前 then 方法 return 的值:
    • return 当前 promise,循环引用是不被允许的。(由于需要判断循环引用问题,需将 then 方法的回调放到 setTimeout 中做一个延迟执行,才能拿到当前 promise,方便与 return 的 promise做对比)。
    • return 新的 promise,调用该 promise 的 then 方法即可。
    • return 其他非 promise 的值,resolve(v) 即可。
class MyPromise {
  // ... 
  then(successCallback, failCallback) {
    successCallback = successCallback || ((value) => value);
    failCallback =
      failCallback ||
      ((reason) => {
        throw reason;
      });
    const p = new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        // setTimeout 不能提取到 handleThenCallback 中,不然就拿不到 p
        setTimeout(() => {
          handleThenCallback(p, successCallback, this.value, resolve, reject);
        }, 0);
      } else if (this.status === REJECTED) {
        setTimeout(() => {
          handleThenCallback(p, failCallback, this.reason, resolve, reject);
        }, 0);
      } else {
        this.successCallbacks.push(() => {
          setTimeout(() => {
            handleThenCallback(p, successCallback, this.value, resolve, reject);
          }, 0);
        });
        this.failCallbacks.push(() => {
          setTimeout(() => {
            handleThenCallback(p, failCallback, this.reason, resolve, reject);
          }, 0);
        });
      }
    });
    return p;
  }
}

/**
 * then 方法失败/成功回调的执行、处理
 * @param {*} p then 方法返回的新 promise
 * @param {*} callback then 方法传入的成功/失败回调函数
 * @param {*} data resolve 的 value,或 reject 的 reason
 * @param {*} resolve
 * @param {*} reject
 */
function handleThenCallback(p, callback, data, resolve, reject) {
  try {
    const v = callback(data);
    handleThenCallbackReturnValue(p, v, resolve, reject);
  } catch (error) {
    reject(error);
  }
}

/**
 * 根据 then 方法 成功/失败回调返回值,改变新 promise 的状态
 * @param {*} p then 方法返回的新 promise
 * @param {*} v 成功或失败回调执行后返回的值
 * @param {*} resolve
 * @param {*} reject
 */
function handleThenCallbackReturnValue(p, v, resolve, reject) {
  if (p === v) {
    throw new TypeError("Chaining cycle detected for promise #<Promise>");
  }
  if (v instanceof MyPromise) v.then(resolve, reject);
  else resolve(v);
}

Promise.prototype.catch()

catch 方法是 .then(null, failCallback)或.then(undefined, failCallback) 的别名,用于指定发生错误时的回调函数。

class MyPromise {
  catch(failCallback) {
    return this.then(undefined, failCallback);
  }
}

Promise.prototype.finally() [ES2018]

finally 方法,接收一个普通的回调函数(不管 promise 最后状态如何都会执行该回调),返回 promise(传递 value/reason)

class MyPromise {
  finally(callback) {
    return this.then(
      (value) => MyPromise.resolve(callback()).then(() => value),
      (reason) =>
        MyPromise.resolve(callback()).then(() => {
          throw reason;
        })
    );
  }
}

静态方法(将参数包装成Promise)

Promise.resolve()

resolve 方法把传入的参数,转换成 promise,状态为 fulfilled。

  • 参数为 promise,原样返回。
  • 参数为 thenable 的对象,包装成 promise,并立即执行 then 方法。
  • 参数为其他对象或基础类型值,resolve 的 value 为该参数。
  • 没有参数,返回 resolved 状态的 promise。
class MyPromise {
  static resolve(value) {
    if (value instanceof MyPromise) return value;
    else if (value && typeof value.then === "function") {
      // thenable 的对象,该如何判断 then 方法是 thenable 呢?
      // let thenable = {
      //     then: (resolve, reject) => resolve(42)
      // }
    } else {
      return new MyPromise((resolve, reject) => {
        resolve(value);
      });
    }
  }
}

Promise.reject()

reject 方法把传入的参数,转换成 promise,状态为 rejected。reject 的 reason 为传入的参数(原封不动)。

class MyPromise {
  static reject(value) {
    return new MyPromise((resolve, reject) => {
      reject(value);
    });
  }
}

静态方法(将多个Promise包装成一个Promise)

  1. Promise.all/race/allSettled/any(),这四种静态方法,都是将多个 Promise 实例,包装成一个新的 Promise 实例。
  2. 接收一个具有 Iterator 接口的参数,如果参数项不是 promise,会先用 Promise.resolve() 包装。

Promise.all()

  • 所有参数项实例的状态都变成 fulfilled,包装实例的状态才会变成 fulfilled。resolve 的 value 为所有 Promise 实例返回值组成的数组。
  • 任何参数项实例的状态变成 rejected,包装实例的状态变成 rejected。reject 的 reason 为第一个 reject 实例的返回值。
class MyPromise {
  static all(array) {
    const real_array = iterator2Array(array);
    return new MyPromise((resolve, reject) => {
      let result = [],
        len = real_array.length;
      const addDataToResult = (index, value) => {
        result[index] = value;
        if (--len <= 0) resolve(result);
      };
      real_array.forEach((item, index) => {
        item = item instanceof MyPromise ? item : MyPromise.resolve(item);
        item.then(
          (value) => addDataToResult(index, value),
          (reason) => reject(reason)
        );
      });
    });
  }
}

/**
 * 将具有 Iterator 接口转化为数组,如果参数不具有 Iterator 接口,抛出错误
 * @param {*} v 待转化的参数
 */
function iterator2Array(v) {
  let real_array = [];
  try {
    for (let i of v) real_array.push(i);
  } catch (e) {
    throw new Error(e.message);
  }
  return real_array;
}

Promise.race()

最先改变状态的一项实例,决定了包装实例的最后状态。

class MyPromise {
  static race(array) {
    const real_array = iterator2Array(array);
    return new MyPromise((resolve, reject) => {
      real_array.forEach((item, index) => {
        item = item instanceof MyPromise ? item : MyPromise.resolve(item);
        item.then(
          (value) => resolve(value),
          (reason) => reject(reason)
        );
      });
    });
  }
}

Promise.allSettled() [ES2020]

所有参数项实例都返回结果后,包装实例的状态变成fulfilled。resolve 的 value 为一个数组,数组每一项是状态和返回值构成的对象。

class MyPromise {
  static allSettled(array) {
    const real_array = iterator2Array(array);
    return new MyPromise((resolve, reject) => {
      let result = [],
        len = real_array.length;
      const addDataToResult = (index, value) => {
        result[index] = value;
        if (--len <= 0) resolve(result);
      };
      real_array.forEach((item, index) => {
        item = item instanceof MyPromise ? item : MyPromise.resolve(item);
        item.then(
          (value) => addDataToResult(index, { status: "fulfilled", value }),
          (reason) => addDataToResult(index, { status: "rejected", reason })
        );
      });
    });
  }
}

Promise.any() [ES2021]

  • 任何一个参数项实例变成 fulfilled 状态,包装实例变成 fulfilled 状态。
  • 所有参数项实例变成 rejected 状态,包装实例变成 rejected 状态。抛出的错误是 AggregateError 实例(多个错误包装在一个错误内)。
class MyPromise {
  static any(array) {
    const real_array = iterator2Array(array);
    return new MyPromise((resolve, reject) => {
      let len = real_array.length;
      const reasons = [];
      const addDataToErr = (index, reason) => {
        reasons[index] = reason;
        if (--len <= 0) {
          const err = new AggregateError(reasons, "All promises were rejected");
          reject(err);
        }
      };
      real_array.forEach((item, index) => {
        item = item instanceof MyPromise ? item : MyPromise.resolve(item);
        item.then(
          (value) => resolve(value),
          (reason) => addDataToErr(index, reason)
        );
      });
    });
  }
}

未解决的问题

  1. 静态方法 resolve, 参数为 thenable 的对象,不知道如何判断 thenable 的对象?
  2. throw Error, 不知道如何测试?

完整代码 MyPromise,放到码云上了。还用 mocha + istanbul 写了测试代码。