实现一个符合Promises/A+规范的Promise

541 阅读8分钟

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。

promise的基本用法

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('do something...');
    const value = 'some result';
    resolve(value);
  }, 1000);
});

p.then(
  value => {
    console.log(value);
  },
  err => {
    console.log(err);
  }
);

// promise 也可以链式调用
new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('do something...');
    const value = 'some result';
    resolve(value);
  }, 1000);
}).then(
  value => {
    console.log(value);
  },
  err => {
    console.log(err);
  }
);

实现一个符合Promise/A+规范的Promise

Promises/A+

  • Promise默认是一个类,用的时候需要new,创建的实例上都有一个then方法,在new过程中需要传入一个executor执行器参数
    • pomise有三个状态,分别为pending(等待态)、fulfilled(成功态)和rejected(失败态)
    • Promise中有一个value属性用来描述成功的返回值,reason用描述失败的原因
    • promise中如果出现异常也会执行失败的逻辑
    • 当promise状态是pending的时候,可以转为fulfilled或者rejected,过程是不可逆的,也就是不能从fulfilled或者rejected转为其他状态
    • executor会立即执行,并且接受resolve和reject两个参数,这两个参数都是函数类型,调用resolve可以将promise转为fulfilled状态,调用reject可以将promise转为rejected状态。

参照上面的解析,我们可以写出第一版最初始的promise

// 定义promise状态
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;

    const resolve = value => {
      // 只能由pending态转为其他态
      if (this.status !== PENDING) return;
      this.status = FULFILLED;
      this.value = value;
    };

    const reject = reason => {
      // 只能由pending态转为其他态
      if (this.status !== PENDING) return;
      this.status = REJECTED;
      this.reason = reason;
    };

    try {
      executor(resolve, reject);
    } catch (e) {
      // 执行executor过程中出错
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
  }
}

但是现在实现的代码非常脆弱,只能实现状态变为fulfilled或者rejected时再调用then,但是在Promises/A+规范中,then可以随时调用,并且同一个实例可以多次调用then。所以我们稍稍改变一些代码,用两个数组来分别存放pending状态时调用then的onFulfilled和onRejected回调函数,当调用resolve改变promise状态时,再分别从数组中取出回调函数依次执行。这样就能解决这两个问题。

// 定义promise状态
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;
    // 用来存放成功的回调函数
    this.onFulfilledCallbacks = [];
    // 用来存放失败的回调函数
    this.onRejectedCallbacks = [];

    const resolve = value => {
      // 只能由pending态转为其他态
      if (this.status !== PENDING) return;
      this.status = FULFILLED;
      this.value = value;
      this.onFulfilledCallbacks.forEach(cb => cb(this.value));
    };

    const reject = reason => {
      // 只能由pending态转为其他态
      if (this.status !== PENDING) return;
      this.status = REJECTED;
      this.reason = reason;
      this.onRejectedCallbacks.forEach(cb => cb(this.reason));
    };

    try {
      executor(resolve, reject);
    } catch (e) {
      // 执行executor过程中出错
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
    if (this.status === PENDING) {
      // 有可能调用then的时候既没成功也没失败,将回调存起来(发布订阅模式)
      this.onFulfilledCallbacks.push(onFulfilled);
      this.onRejectedCallbacks.push(onRejected);
    }
  }
}

这样看起来很完美,但是Promises/A+规范要求then方法能链式调用,并且

  • promise中then传入的方法, 返回的结果不是一个promise,会将这个结果传递给下一个then的成功中
  • 如果then传入的方法在执行的时候出错了,会执行下一次then的失败
  • 如果then传入的方法执行返回的是一个promise,那么会根据promise的状态来决定走下一次then的成功还是失败,成功的值和失败的原因以当前这个promise为准

所以这里我们改造一下then方法,支持调用回调时返回一个不是promise的情况:

then(onFulfilled, onRejected) {
    // 为了支持链式调用,我们这里返回一个新的promise实例
    const p = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        try {
          const x = onFulfilled(this.value);
          resolve(x);
        } catch (e) {
          // 执行回调的时候出错,直接reject
          reject(e);
        }
      }
      if (this.status === REJECTED) {
        try {
          const x = onRejected(this.reason);
          resolve(x);
        } catch (error) {
          // 执行回调的时候出错,直接reject
          reject(e);
        }
      }
      if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(() => {
          try {
            const x = onFulfilled(this.value);
            resolve(x);
          } catch (e) {
            // 执行回调的时候出错,直接reject
            reject(e);
          }
        });
        this.onRejectedCallbacks.push(() => {
          try {
            const x = onRejected(this.reason);
            resolve(x);
          } catch (e) {
            // 执行回调的时候出错,直接reject
            reject(e);
          }
        });
      }
    });
    return p;
  }

下面我们来实现promise中最复杂的情况,也就是当执行onFulfilled或者onRejected回调函数时返回一个promise的情况,我们定义一个resolvePromise函数,用来专门处理执行onFulfilled或者onRejected后的剩余处理:

function resolvePromise(promise, x, resolve, reject) {
  // 用x的值来决定promise走resolve还是reject

  // 不能自己等待自己完成
  if (promise === x) {
    return reject(new TypeError(`TypeError: Chaining cycle detected for promise #<Promise> `));
  }

  // 思考: 这里能不能使用 x instanceof Promise 呢?
  // 我们要考虑和不同人写的promise可以相互兼容,所以这里这里要按照规范来实现,保证promise之间可以相互调用, 所以这里不能用 x instanceof Promise
  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // 这个then方法可能是别人实现的promise中的then,没有处理同时调用成功和失败,
    // 用一个变量来标识 只能调用resolve或reject, 不能两者都调用
    let called = false;
    try {
      const then = x.then;
      if (typeof then === 'function') {
        // 有可能 x = { then: 111 }
        // 执行结果为一个promise实例时
        then.call(
          x,
          v => {
            if (called) return;
            called = true;
            // 递归判断是否任然返回的是promise
            resolvePromise(promise, v, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        // 依旧是普通值
        resolve(x);
      }
    } catch (e) {
      // 这里我们为什么要用try catch呢,原因是其他promise的实现中then可能是用defineProperty定义的, 当调用x.then会出错, 这里为了更加严谨最好包一层 try catch
      /**
        Object.defineProperty(x, 'then', {
          get() {
            throw new Error()
          }
        })
       */
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 执行返回结果为普通值
    resolve(x);
  }
}


then(onFulfilled, onRejected) {
    // 为了支持链式调用,我们这里返回一个新的promise实例
    const p = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        // try {
        //   const x = onFulfilled(this.value);
        //   // 这里不能直接这样写,因为此时p还没有值,我们不能在new的过程中获取到实例
        //   // resolvePromise(p, x, resolve, reject);

        //   // 我们在下一个事件循环tick中就能获取到p实例了
        //   setTimeout(() => {
        //     resolvePromise(p, x, resolve, reject);
        //   });
        // } catch (e) {
        //   reject(e);
        // }
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(p, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }
      if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(p, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }
      if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(p, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          });
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(p, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          });
        });
      }
    });
    return p;
  }

上面我们使用了setTimeout,这样就能在下一个事件循环tick中获取到返回的promise的实例了,但是setTimeout会将回调放入宏任务队列。我们都知道原生的promise是将其放入微任务队列中的,所以这里我们可以用queueMicrotask来代替setTimeout。

到这里,Promise的实现基本就算完成了,但是还有一个细节要注意,当调用then时,可把第一个参数设置undefined,只写onRejected, 这样在后面的then中也能实现获取到上一个promise resolve的value。同样我们可以不写then中的第二个参数,在后面的then中也能获取到上一个promise reject的reason,实现穿透效果。

主要实现一个方法可以层层传递:

onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e };

最终我们就实现了一个完整版的遵循Promises/A+规范的promise,注意我们平时用到的Promise.resolve、Promise.reject、Promise.all、Promise.race、finally等方法不属于Promises/A+规范中的定义

这里我们也来实现一下:

resolve

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

reject

static reject(value) {
  // 默认创建一个失败的promise
  return new Promise((resolve, reject) => {
    reject(value);
  });
}

catch

catch(errCallback) {
  return this.then(null, errCallback);
}

all

static all = function (promises) {
  let result = [];
  let times = 0;
  return new Promise((resolve, reject) => {
    function processResult(data, index) {
      result[index] = data; // 映射结果
      if (++times == promises.length) {
        resolve(result);
      }
    }
    for (let i = 0; i < promises.length; i++) {
      let promise = promises[i];
      Promise.resolve(promise).then(data => {
        processResult(data, i);
      }, reject);
    }
  });
};

race

static race = function (promises) {
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises.length; i++) {
      let promise = promises[i];
      Promise.resolve(promise).then(resolve, reject);
    }
  });
};

finally

finally(finallyCallback) {
  return this.then(
    data => {
      Promise.resolve(finallyCallback()).then(() => data);
    },
    err => {
      return Promise.resolve(finallyCallback()).then(() => {
        throw err;
      });
    }
  );
}

allSettled

static allSettled = function (promises) {
  let result = [];
  let times = 0;
  return new Promise((resolve, reject) => {
    function processResult(data, index, status) {
      result[index] = { status, value: data };
      if (++times == promises.length) {
        resolve(result);
      }
    }
    for (let i = 0; i < promises.length; i++) {
      let promise = promises[i];
      Promise.resolve(promise).then(
        data => {
          processResult(data, i, 'fulfilled');
        },
        err => {
          processResult(err, i, 'rejected');
        }
      );
    }
  });
};

最终版的代码如下:

const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    return reject(new TypeError(`TypeError: Chaining cycle detected for promise #<Promise> `));
  }
  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    let called = false;
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          v => {
            if (called) return;
            called = true;
            resolvePromise(promise, v, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = value => {
      // 这里我们添加一个规范外的逻辑 让value值是promise的话可以进行一个解析
      if(value instanceof Promise){
         // 递归解析值
         return value.then(resolve, reject)
      }
      if (this.status !== PENDING) return;
      this.status = FULFILLED;
      this.value = value;
      this.onFulfilledCallbacks.forEach(cb => cb(this.value));
    };

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

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

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : e => {
            throw e;
          };
    const p = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(p, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }
      if (this.status === REJECTED) {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(p, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }
      if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(() => {
          queueMicrotask(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(p, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          });
        });
        this.onRejectedCallbacks.push(() => {
          queueMicrotask(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(p, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          });
        });
      }
    });
    return p;
  }

  catch(errCallback) {
    return this.then(null, errCallback);
  }

  finally(finallyCallback) {
    return this.then(
      data => {
        Promise.resolve(finallyCallback()).then(() => data);
      },
      err => {
        return Promise.resolve(finallyCallback()).then(() => {
          throw err;
        });
      }
    );
  }

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

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

  static all = function (promises) {
    let result = [];
    let times = 0;
    return new Promise((resolve, reject) => {
      function processResult(data, index) {
        result[index] = data; // 映射结果
        if (++times == promises.length) {
          resolve(result);
        }
      }
      for (let i = 0; i < promises.length; i++) {
        let promise = promises[i];
        Promise.resolve(promise).then(data => {
          processResult(data, i);
        }, reject);
      }
    });
  };

  static race = function (promises) {
    return new Promise((resolve, reject) => {
      for (let i = 0; i < promises.length; i++) {
        let promise = promises[i];
        Promise.resolve(promise).then(resolve, reject);
      }
    });
  };

  static allSettled = function (promises) {
    let result = [];
    let times = 0;
    return new Promise((resolve, reject) => {
      function processResult(data, index, status) {
        result[index] = { status, value: data };
        if (++times == promises.length) {
          resolve(result);
        }
      }
      for (let i = 0; i < promises.length; i++) {
        let promise = promises[i];
        Promise.resolve(promise).then(
          data => {
            processResult(data, i, 'fulfilled');
          },
          err => {
            processResult(err, i, 'rejected');
          }
        );
      }
    });
  };
}

module.exports = Promise;

最后来测试一下我们写的代码是否完全符合Promises/A+规范,可以安装 promises-aplus-tests 这个包进行测试。

我们还需要添加一些 promises-aplus-tests测试需要用的代码:

// 为了测试是否符合规范需要导出的
Promise.deferred = function () {
  const dfd = {};
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
};

最后执行

npx promises-aplus-tests myPromise.js

可以看到我们通过了所有的测试用例~

image-20220122160202033.png