你还在为如何手写Promise烦恼么

148 阅读8分钟

背景介绍:

传统的异步串行方案多以回调的形式进行,在这种模式中,上一个的输出作为下一个的输入,数量过多就会产生”回调地狱“问题。Promise以链式调用的形式解决了传统“回调地狱”的问题,以更加直观的形式来进行回调。其次,Promise还能很好地解决异步并发问题,比如借助Promise.all方案。最后,Promise的错误处理机制,让错误处理变得十分简单。

Promise的基本概念:

Promise是异步编程的一种解决方案,简单来说,它就是一种容器,里面保存着某个未来才会结束的事件(比如一些异步操作)。它主要有三个状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态的变化,只能是pending到fulfilled或者pending到rejected,反之则不行,即状态一旦转移,不可再改变。

话不多说,接下来我们看看如何实现一个完整的Promise。

Promise的实现思路:

Promise的实现遵循Promises/A+规范,在此基础上,我们还引入了Promise es6的一些实现,比如Promise.all、Promise、race、Promise.prototype.finally等扩展实现。我将整个实现过程分为以下3个步骤:

  1. Promise的基础框架搭建;
  2. Promise.then实现(到这里,Promise/A+规范的内容就已经结束了)
  3. Promise es6扩展实现(Promise.all、Promise、race、Promise.prototype.finally等等)

Pomise的实现:

Promise的基础框架搭建:

  1. Promise的三种状态pending、fulfilled、rejected;
  2. 成功、失败的回调;
  3. Promise.then基础框架。 这部分代码比较简单,我就不过多解释,代码如下:
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';
class MyPromise {
    constructor(fn) {
        this.state = PENDING; //初始状态
        const resolve = (value) => {
            if (this.state !== PENDING) return;
            this.state = RESOLVED;
            this.value = value;
        }
        const reject = (reason) => {
            if (this.state !== PENDING) return;
            this.state = REJECTED;
            this.reason = reason;
        }
        try {
            fn(resolve, reject);
        } catch(err) {
            reject(err); //调用fn出错时,直接reject
        }
    }
    then(onFulFilledCb, onRejectedCb) {
        if (this.state === PENDING) {
            //todo...
        } else if (this.state === RESOLVED) {
            onFulFilledCb(this.value)
        } else if (this.state === REJECTED) {
            onRejectedCb(this.reason)
        }
    }
}

Promise.then实现:

基于上面的基础框架,我们来思考一些问题。如果Promise传入的是一个异步的函数,此时在执行到.then回调时状态还是pending态,这时候该怎么办?Promise.then之后是可以继续进行.then的,这里我们该怎么实现?如果在执行成功或者错误的回调时候,又抛错了,又该如何处理呢?等等。结合A+规范,下面我们将Promise.then拆分成4个小步骤:

  1. 处理Promise传入异步函数,.then状态仍然是pending的情况: 虽然此时不会立即去执行成功或者失败的回调,但是我们想要在接下来状态改变之后就去执行相应的回调。而且,同一个Promise是可以被多次.then的,多次.then的回调按照写入的顺序执行。所以我用俩数组分别去存储成功和失败的回调,在状态改变之后遍历执行即可。并且在执行回调的过程中,如果出现异常,也会被捕获。代码如下:
class MyPromise {
    constructor(fn) {
        this.state = PENDING; //初始状态
        this.onFulfilledCbs = []; //存储成功的回调
        this.onRejectedCbs = []; //存储失败的回调
        const resolve = (value) => {
            if (this.state !== PENDING) return;
            this.state = RESOLVED;
            this.value = value;
            this.onFulfilledCbs.forEach(cb => cb());
        }
        const reject = (reason) => {
            if (this.state !== PENDING) return;
            this.state = REJECTED;
            this.reason = reason;
            this.onRejectedCbs.forEach(cb => cb());
        }
        try {
            fn(resolve, reject);
        } catch(err) {
            reject(err); //调用fn出错时,直接reject
        }
    }
    then(onFulFilledCb, onRejectedCb) {
        if (this.state === PENDING) {
            this.onFulfilledCbs.push(() => {
                onFulFilledCb(this.value);
            })
            this.onRejectedCbs.push(() => {
                onRejectedCb(this.reason);
            })
        } else if (this.state === RESOLVED) {
            onFulFilledCb(this.value)
        } else if (this.state === REJECTED) {
            onRejectedCb(this.reason)
        }
    }
}

2.Promise.then返回的是一个新的Promise,可以被链式调用: 根据A+规范.then必须返回一个新的Promise,而且可以持续的进行then链的调用。.then的回调应该被当作一个函数来调用,即使传入的不是一个函数(这个就是“穿透”的概念,即经过几层then链调用之后,仍然可以在最后拿到之前resolve的值)在执行then回调的过程中如果出现异常,也会被捕获,代码如下:

then(onFulFilledCb, onRejectedCb) {
        onFulFilledCb = typeof onFulFilledCb === 'function' ? onFulFilledCb : v => v;
        onRejectedCb = typeof onRejectedCb === 'function' ? onRejectedCb : err => {throw err};
        return new MyPromise((resolve, reject) => {
            if (this.state === PENDING) {
                this.onFulfilledCbs.push(() => {
                    try {
                        const x = onFulFilledCb(this.value);
                        resolve(x);
                    } catch (err) {
                        reject(err)
                    }
                })
                this.onRejectedCbs.push(() => {
                    try {
                        const x = onRejectedCb(this.reason);
                        resolve(x);
                    } catch (err) {
                        reject(err);
                    }
                })
            } else if (this.state === RESOLVED) {
                try {
                    const x = onFulFilledCb(this.value);
                    resolve(x);
                } catch (err) {
                    reject(err);
                }
            } else if (this.state === REJECTED) {
                try {
                    const x = onRejectedCb(this.reason);
                    resolve(x);
                } catch (err) {
                    reject(err);
                }
            }
        })

    }

3.根据规范onResolvedCb和onRejetedCb必须被异步的执行,在事件循环转化之后的一个新的stack中回调才会被调用: 这里读起来有点拗口,A+规范里面就是这么去描述的。被“异步的执行”,这里规范里面指出可以有多种方式,比如宏任务的setTimeout和setImmediate的形式,或者微任务中MutationObserver和 process.nextTick的形式,都可以。es6里面应该是微任务的形式去实现,所以我们常说Promise.then里的执行时机,是以一个微任务的形式就是这么个道理。这里,我们用setTimeout宏任务这样一个比较简单的形式去实现,代码比较简单,就是将两种类型的回调进行setTimeout包裹,这里不再单独贴代码。 4. then传递成功或失败的函数,针对它的返回值会做不同处理。 这是then实现最难也是最复杂的一个地方。根据成功或失败回调函数的返回值x,会有以下情况出现:

  • 如果x不是一个Promise,则x会被作为下一个then的成功回调结果;
  • 想要走到下一个then的失败回调,那么必须在当前then的回调中抛出一个错误;
  • 如果x“是”一个Promise,那么下一个then会走怎样的回调,取决于x这个Promise的状态怎么变化。(为什么这里的是字打了引号,是因为x.then属性存在,且then是一个函数,那么x就被看作是一个Promise来处理)而且,如果x和当前then返回的Promise是同一个则会报错。

在A+规范里面,这个处理过程被称为是Promise Resolution Procedure,出了上面三点,有些细节性的问题,我直接在代码里面会作出注解,以下是代码实现:

const resolutionProcedure = (promise, x, resolve, reject) => {
  if (promise === x) {
    //如果x和promise2是同一个对象,则永远也走不到下一步,进入一个死循环了
    return reject(new TypeError("Chaining cycle detected for promise"));
  }
  if ((typeof x === "object" && x !== null) || typeof x === "function") {
    //规范里面指出了,走了成功或者失败,就不能再走一次成功或者失败,所以用一个变量来保存是否走过成功或者失败。
    let isCalled = false;
    try {
      const then = x.then;
      if (typeof then === "function") {
        //调用then函数,并且修改其this指向为x
        then.call(
          x,
          (y) => {
            if (isCalled) return;
            isCalled = true;
            //走了成功的回调,此时如果y又是一个Promise,那么我们最终的状态还是取决于y Promise,
            //所以对于y值进行递归的resolutionProcedure处理
            resolutionProcedure(promise, y, resolve, reject);
          },
          (r) => {
            if (isCalled) return;
            isCalled = true;
            //走的是失败的回调,与成功的时候对应,但是这里不需要递归处理,
            //因为一旦走到reject,不管返回的是不是一个Promise,直接进入到下一个then的reject回调中
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (err) {
      if (isCalled) return;
      isCalled = true;
      reject(err);
    }
  } else {
    //除去上述情况,都会走到resolve来
    resolve(x);
  }
};
then(onFulFilledCb, onRejectedCb) {
    onFulFilledCb =
      typeof onFulFilledCb === "function" ? 
      onFulFilledCb : (v) => v;
    onRejectedCb =
      typeof onRejectedCb === "function" ? 
        onRejectedCb : (err) => {throw err;};
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === PENDING) {
        this.onFulfilledCbs.push(() => {
          setTimeout(() => {
            try {
              const x = onFulFilledCb(this.value);
              resolutionProcedure(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          })
        });
        this.onRejectedCbs.push(() => {
          setTimeout(() => {
            try {
              const x = onRejectedCb(this.reason);
              resolutionProcedure(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          })
        });
      }
      if (this.state === FULFILLED) {
        setTimeout(() => {
          try {
            const x = onFulFilledCb(this.value);
            resolutionProcedure(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        })
      }
      if (this.state === REJECTED) {
        setTimeout(() => {
          try {
            const x = onRejectedCb(this.reason);
            resolutionProcedure(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        })
      }
    });
    return promise2;
  }

到这里Promise A+规范的整个实现就算大功高成了,A+规范网站上提供了测试用的库promises-aplus-tests 读者可以自行拷贝代码,然后测试一下。接下来我们再看看关于es6里面提供的Promise的一些相关扩展方法实现。

es6里部分Promise扩展方法实现:

私有属性上面的Promise.resolve、Promise.reject、Promise.all、Promise.race、Promise.prototype.finally实现,直接上代码:

  static resolve(value) {
    return new MyPromise((resolve) => {
      resolve(value);
    });
  }
  static reject(err) {
    return new MyPromise(
      (null,
      (reject) => {
        reject(err);
      })
    );
  }
  static all(promises) {
    return MyPromise((resolve, reject) => {
      let counts = 0,
        results = [];
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          (res) => {
            results[index] = res;
            counts++;
            if (counts === promises.length) resolve(res);
          },
          reject
        );
      });
    });
  }
  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach((promise) => {
        MyPromise.resolve(promise).then(resolve, reject)
      })
    })
  }
  finally(callback) {
    const P = this.constructor;
    return this.then(
      value  => P.resolve(callback()).then(() => value),
      reason => P.resolve(callback()).then(() => { throw reason })
    );
  };

需要注意一点的就是all实现中,不是判断index === promises.length,因为很可能最后一个Promise先完成,但是前面尚未完成,但此时不应该resolve。以及finally,不管成功或者失败都会执行,而且才cb中拿不到关于成功或者失败的返回值,但是在finnaly之后.then中能难道成功或者失败的值。

写在最后:

以上就是本次分享关于Promise实现的全部内容,当然有些代码可以优化,比如then里面多次setTimeout部分。欢迎大家一起讨论哈,好久不写文章,有些生疏,多多包含。要是给个小小的点赞就最好不过了,谢谢大家~。

相关链接:

Promise A+规范

阮一峰大佬es6 Promise部分