0%

手写 Promise

尝试使用 js 手写 Promise 类,实现部分功能,遵守 Promises/A+ 规范,例如 then, catch, all, race, resolve, reject 等。

使用原生 Promise 查看效果

let promise1 = new Promise((resolve, reject) => {
  resolve(1);
});

promise1
  .then(res => {
    console.log(res);
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(2);
      }, 1000);
    });
  })
  .then(res => {
    console.log(res);
    throw new Error();
  })
  .catch(err => {
    console.log(err);
  });
// 1
// 1秒后
// 2
// Error

从上述代码中可以看出 promise 的几个性质,首先它是一个类,可以被实例化,参数为一个函数,有 resolvereject 两个参数。当期望的事情完成后,调用 resolve 表明成功,调用 reject 表明失败。成功后可以调用 .then 来进行后续操作,默认参数为 resolve 函数的参数。返回值可以是一个新的 promise ,后续操作会等待这个 promise 的完成。抛出错误时或是手动调用 reject 都会进入 catch 回调,或是 .then 方法的第二个参数。

接下来,我们一步步实现。

实现过程

  1. 定义 promise 构造函数

    首先定义三个常量用于表示状态,Promises/A+ 规范中提到:

    A promise must be in one of three states: pending, fulfilled, or rejected.

    1. When pending, a promise:

      1. may transition to either the fulfilled or rejected state.
    2. When fulfilled, a promise:

      1. must not transition to any other state.
    3. must have a value, which must not change.

    4. When rejected, a promise:

      1. must not transition to any other state.
    5. must have a reason, which must not change.

    Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.

    promise 仅有三种状态,表示还未完成,已成功或是已失败。当处于 pending 状态时,可能会转换为另外两种状态。当处于 fulfilled 状态时,不会再转为其余状态,必须拥有一个 value 属性且不再变化。当处于 rejected 状态时,同样不会再转变,且必须拥有一个 reason 属性不再变化。

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

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

   const resolve = () => {};

   const reject = () => {};

   executor(resolve, reject);
 }

}


   所以在构造函数中,我们定义三个变量,初始化此时的状态为 `pending` ,并且传入的 `executor` 函数我们需要进行调用,传入定义的两个函数作为参数。

   然后定义 `resolve` 和 `reject` 的内容。

   ```js
const resolve = value => {
     if (this.status !== PENDING) return;
  this.status === FULFILLED;
     this.value = value;
};

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

遵循规范,若当前状态不是 pending ,不执行后续操作。

  1. 定义 then 方法

    首先查看规范:

    A promise’s then method accepts two arguments:

    promise.then(onFulfilled, onRejected)
    1. Both onFulfilled and onRejected are optional arguments:

      1. If onFulfilled is not a function, it must be ignored.
      2. If onRejected is not a function, it must be ignored.
    2. If onFulfilled is a function:

      1. it must be called after promise is fulfilled, with promise’s value as its first argument.
      2. it must not be called before promise is fulfilled.
      3. it must not be called more than once.
    3. If onRejected is a function,

      1. it must be called after promise is rejected, with promise’s reason as its first argument.
      2. it must not be called before promise is rejected.
      3. it must not be called more than once.
    4. onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

    5. onFulfilled and onRejected must be called as functions (i.e. with no this value). [3.2]

    6. thenmay be called multiple times on the same promise.

      1. If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then.
      2. If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then.
    7. then must return a promise [3.3].

       promise2 = promise1.then(onFulfilled, onRejected);
      1. If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).
      2. If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
      3. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
      4. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

    规范提到,then 方法的两个参数是可选的,但必须是函数,不是函数的话将被忽略。

    onFulfilled 将在 promise 状态为 fulfilled 之后调用,promisevalue 作为它的参数,在 promise 转为 fulfilled 之前不允许调用,且只能被调用一次。 onRejected 类似。

    两个函数都只能在执行上下文调用栈仅包含 platform code 时调用,需要确保两个函数都是异步调用的。

    两个函数都必须以函数形式调用,不允许包含 this

    同一个 promise 可以多次调用 then 方法(此处不是指链式调用),需要确保多次调用是按照顺序执行的。

    then 方法的返回值也是一个 promise

    onFulfilled 或是 onRejected 的返回值需要执行 Promise Resolution Procedure

    两个函数若是抛出了错误,promise2 需要以该错误为参数被拒绝。

    若没有传入两个函数,promise 值需要同样往后续传递。

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

    首先简单实现同步回调,判断当前的 status 来执行相应的回调。

    接下来考虑异步执行,若是 executor 内部是一个异步执行,异步执行中才会调用 resolve 或是 reject ,那么我们在调用 then 时, status 还没有得到转换,此时无法正确执行,且需要在异步地更改 status 后再执行回调。

    修改代码:

    // ...
    
    class MyPromise {
      constructor(executor) {
        // ...
        this.onFulfilledCallList = [];
        this.onRejectedCallList = [];
    
        const resolve = value => {
          // ...
          this.onFulfilledCallList.forEach(fn => fn());
        };
    
        const reject = reason => {
          // ...
          this.onRejectedCallList.forEach(fn => fn());
        };
    
        // ...
      }
      then(onFulfilled, onRejected) {
        // ...
        if (this.status === PENDING) {
          this.onFulfilledCallList.push(() => {
            onFulfilled(this.value);
          });
          this.onRejectedCallList.push(() => {
            onRejected(this.reason);
          });
        }
      }
    }

    此时的异步 resolve 已经可以正确执行。

    再来考虑错误捕获的问题,我们的 executor 中可能会抛出错误,此时需要直接执行 reject 来拒绝。

    修改代码:

    // ...
    
    class MyPromise {
      constructor(executor) {
        // ...
    
        try {
          executor(resolve, reject);
        } catch (e) {
          reject(e);
        }
      }
      // ...
    }

    在执行 executor 时就尝试捕获错误,若是捕获到了就直接 reject ,此时需要定义 then 的第二个参数来处理。

    此时若是对 promise 实例多次调用 then 方法也可以正确且按序执行。

    接下来处理两个参数为空的情况。

    // ...
    
    class MyPromise {
      // ...
      then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected =
          typeof onRejected === 'function'
            ? onRejected
            : reason => {
                throw reason;
              };
        // ...
      }
    }

    接下来处理 then 的返回值问题,即实现链式调用。

    参考原生 promise 的表现形式:

    let promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1);
      }, 1000);
    });
    
    promise1
      .then(
        res => {
          console.log('10', res); // 10 1
          return '11';
        }
      )
      .then(
        res => {
          console.log('16', res); // 16 11
          return new Promise((resolve, reject) => {
            resolve('18');
          });
        }
      )
      .then(
        res => {
          console.log('24', res); // 24 18
        }
      );

    可以看出,每个 then 方法的参数都是前一次 then 的返回值,返回值若是 promise ,则是 resolve 的值。

    于是我们需要处理两种状况,return 了原始值或是 promise

    于是我们对 onFulfilledonRejected 返回值进行一个处理。

    在那之前,我们先注意若是 onFulfilledonRejected 函数内部执行时抛出了错误,我们需要进行 reject 。所以代码修改如下:

    // ...
    
    function promiseResolve(promise2, x, resolve, reject) {
      console.log(promise2, x, resolve, reject);
    }
    
    class MyPromise {
      // ...
      then(onFulfilled, onRejected) {
        // ...
    
        let promise2 = new MyPromise((resolve, reject) => {
          if (this.status === FULFILLED) {
            setTimeout(() => {
              try {
                let x = onFulfilled(this.value);
                promiseResolve(promise2, x, resolve, reject);
              } catch (e) {
                reject(e);
              }
            }, 0);
          }
          if (this.status === REJECTED) {
            setTimeout(() => {
              try {
                let x = onRejected(this.reason);
                promiseResolve(promise2, x, resolve, reject);
              } catch (e) {
                reject(e);
              }
            }, 0);
          }
          if (this.status === PENDING) {
            this.onFulfilledCallList.push(() => {
              try {
                let x = onFulfilled(this.value);
                promiseResolve(promise2, x, resolve, reject);
              } catch (e) {
                reject(e);
              }
            });
            this.onRejectedCallList.push(() => {
              try {
                let x = onRejected(this.reason);
                promiseResolve(promise2, x, resolve, reject);
              } catch (e) {
                reject(e);
              }
            });
          }
        });
        return promise2;
      }
    }

    此处做了较多修改,在 then 方法中定义了一个新的 promise 实例,然后进行状态判定,若是已完成,则进行相应操作。若是未完成,则把回调进行订阅,在完成时进行发布。调用回调时需要得到返回值,然后进行返回值的处理。于是定义了一个 resolvePromise 方法来处理,我们会在这个函数内部进行 resolve 或是 reject ,所以需要传入对应函数。同时传入 promise 本身和 x 这个返回值来自于规范内的规定。

    但是,在调用 promiseResolve 时,我们的 promise2 还没有执行完毕,所以我们需要使用异步方法 setTimeout 来延迟这个函数的执行,确保可以正确得到 promise2 。同时,我们也要对回调函数的执行进行错误捕获,若是捕获到错误就 reject

    为什么订阅回调时不需要异步来调用处理函数呢?因为订阅的回调发布时已经是异步状态。

    然后开始处理 promiseResolve 函数。

    首先看规范:

    The promise resolution procedure is an abstract operation taking as input a promise and a value, which we denote as [[Resolve]](promise, x). If x is a thenable, it attempts to make promise adopt the state of x, under the assumption that x behaves at least somewhat like a promise. Otherwise, it fulfills promise with the value x.

    This treatment of thenables allows promise implementations to interoperate, as long as they expose a Promises/A+-compliant then method. It also allows Promises/A+ implementations to “assimilate” nonconformant implementations with reasonable then methods.

    To run [[Resolve]](promise, x), perform the following steps:

    1. If promise and x refer to the same object, reject promise with a TypeError as the reason.

    2. If x is a promise, adopt its state :

      1. If x is pending, promise must remain pending until x is fulfilled or rejected.
      2. If/when x is fulfilled, fulfill promise with the same value.
      3. If/when x is rejected, reject promise with the same reason.
    3. Otherwise, if x is an object or function,

      1. Let then be x.then. [3.5]

      2. If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.

      3. If then is a function, call it with x as this, first argument resolvePromise , and second argument rejectPromise , where:

        1. If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).

        2. If/when rejectPromise is called with a reason r, reject promise with r.

        3. If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.

        4. If calling then throws an exception e ,

          1. If resolvePromise or rejectPromise have been called, ignore it.

          2. Otherwise, reject promise with e as the reason.

      4. If then is not a function, fulfill promise with x.

    4. If x is not an object or function, fulfill promise with x.

    If a promise is resolved with a thenable that participates in a circular thenable chain, such that the recursive nature of [[Resolve]](promise, thenable) eventually causes [[Resolve]](promise, thenable) to be called again, following the above algorithm will lead to infinite recursion. Implementations are encouraged, but not required, to detect such recursion and reject promise with an informative TypeError as the reason. [3.6]

    简单翻译一下:

    此处使用 promise2 指代第一个参数。

    1. 如果 promise2x 引用的是同一个对象,直接以类型错误 reject

    2. 如果 x 是一个 promise

      1. 如果 x 的状态是 pending ,那么 promise2 需要保持直到 x 转为 fulfilled 或者是 rejected
      2. 如果 x 的状态是 fulfilled ,使用 xvalue 去转换 promise2fulfilled
      3. reject 同上。
    3. 如果 x 是一个对象或函数:

      1. x.then 赋值给 then
      2. 若是 x.then 的结果抛出了错误,使用这个错误来 reject promise2
      3. 如果 then 是一个函数就调用它,第一个参数为 resolvePromise ,第二个参数为 rejectPromise
        • resolvePromise 接收一个参数 y ,它的类型也可能是 promise ,所以调用 promiseResolve 来处理它。
        • rejectPromise 接收一个参数 r ,使用它来 reject promise2
        • 如果两个回调参数都被调用,或是多次使用相同参数重复调用,只有第一次调用生效,其余的会被忽略。
        • 如果调用 x.then 抛出错误:
          • 如果回调参数已经被调用了,忽略。
          • 其余情况,使用错误来 reject promise2
      4. 如果 then 不是一个函数,使用 xfulfilled promise2
    4. 如果 x 是其他类型,使用 xfulfilled promise2

    // ...
    
    function promiseResolve(promise2, x, resolve, reject) {
      if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
      }
      let called = false;
      if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
        try {
          let then = x.then;
          if (typeof then === 'function') {
            then.call(
              x,
              y => {
                if (called) return;
                called = true;
                promiseResolve(promise2, y, resolve, reject);
              },
              r => {
                if (called) return;
                called = true;
                reject(r);
              }
            );
          } else {
            resolve(x);
          }
        } catch (err) {
          if (called) return;
          called = true;
          reject(err);
        }
      } else {
        resolve(x);
      }
    }
    
    class MyPromise {
      // ...
    }

    此处已经可以正确处理链式调用,返回新的 promise 实例的情况。且嵌套返回也没有问题。

    let promise1 = new MyPromise((resolve, reject) => {
      resolve(1);
    });
    
    let promise2 = promise1.then(
      res => {
        return res;
      },
      err => {
        return err;
      }
    );
    promise2
      .then()
      .then()
      .then()
      .then(
        res => {
          console.log('19', res); // 19 1
          return new MyPromise((resolve, reject) => {
            resolve(
              new MyPromise((resolve, reject) => {
                resolve('23');
              })
            );
          });
        },
        err => {
          console.log(err);
        }
      )
      .then(
        res => {
          console.log('34', res); // 34 19
          return new MyPromise((resolve, reject) => {
            resolve('36');
          });
        },
        err => {
          console.log('40', err);
        }
      )
      .then(
        res => {
          console.log('45', res); // 45 34
        },
        err => {
          console.log('48', err);
        }
      );

    接下来实现 catch 方法:

    // ...
    
    class MyPromise {
      // ...
      catch(onRejected) {
        return this.then(null, onRejected);
      }
    }

    如代码所示,catch 的逻辑其实与 then 类似,只是没有传入 onFulfilled 回调而已。

    接下来实现 Promise.resolve Promise.reject 方法:

    // ...
    
    class MyPromise {
      // ...
      static resolve(onResolved) {
        return new MyPromise((resolve, reject) => {
          if (onResolved instanceof MyPromise) {
            onResolved.then(resolve, reject);
          } else {
            resolve(onResolved);
          }
        });
      }
      static reject(onRejected) {
        return new MyPromise((resolve, reject) => {
          reject(onRejected);
        });
      }
    }

    两个函数都返回一个 promise 即可。 resolve 内部需要进行判断,如果传入的值为一个 promise ,那么我们就进行处理。

    接下来实现 Promise.all 方法:

    // ...
    
    class MyPromise {
      // ...
      static all(promises) {
        let len = promises.length;
        let values = [];
        let resolvedCount = 0;
        return new MyPromise((resolve, reject) => {
          promises.forEach(p => {
            MyPromise.resolve(p).then(
              res => {
                values.push(res);
                resolvedCount++;
                if (resolvedCount === len) {
                  resolve(values);
                }
              },
              err => {
                reject(err);
              }
            );
          });
        });
      }
    }

    all 方法等待传入的参数全部执行完成,返回所有返回值的数组。

    我们遍历参数列表,对每一个参数进行 resolve 。将结果加入 value 数组中,并计数。当返回值数量等于参数数量时,可以 resolve 返回值。

    最后实现 race 方法,这个方法只需要 resolve 最先得到结果的值即可。

    // ...
    
    class MyPromise {
      // ...
      static race(promises) {
        return new MyPromise((resolve, reject) => {
          promises.forEach(p => {
            MyPromise.resolve(p).then(
              res => {
                resolve(res);
              },
              err => {
                reject(err);
              }
            );
          });
        });
      }
    }

总结

写完以后看了一下时间,完成时间是 15:58 。花了两个半小时,中间也有一些地方写乱了,然后又去查资料。感觉对 promise 理解加深了一些,但又好像还是很混乱。

总的来说, promise 改善了回调地狱的问题,我们可以在回调中将结果抛出,然后在后续链式调用获取结果。