查看原文
其他

这次彻底搞懂 Promise(手写源码多注释篇)

前端大全 2021-07-15

The following article is from 程序员成长指北 Author 一阵风

(给前端大全加星标,提升前端技能

作者: 程序员成长指北 公号 /  一阵风

前言

promise 是 js 里面非常重要的一部分,搞懂了 promise 才能更好的去理解 async,await 和 generator。但是往往很多时候就是不理解 promise 的机制,所以这次通过一步步实现一个 promise 来加深自己的印象,提高自己的思维。

大概的架子

通过我们经常写的 promise 语法,我们可以先写一个大概的架子出来,promise 接受回调,并且调用,自身带有三种状态,pendding, onFulfilled, onRejected,并且 resolve 这个函数可以让 pendding 状态变成 onFulfilled 状态,同理 reject 函数可以让 pendding 状态变成 onRejected 状态。我们先把上面描述部分实现了。

const PromiseCopy = function (fn) {
  this.info = {
    status"pending",
    value"",
  };

  const self = this;

  self.onFulfilledArr = []; // then函数里面的第一个回调函数的集合
  self.onRejectedArr = []; // then函数里面的第二个回调函数的集合

  const resolve = function (value) {
    // 加这个判断是为了表示,只有在pendding状态下才会去执行
    // 状态已经变成onFulfilled之后就不能再去改变了
    // 符合PromiseA+中的2.1.2.1
    if (self.info.status === "pending") {
      self.info.status = "onFulfilled";

      self.info.value = value;

      self.onFulfilledArr.forEach((fn) => fn(value));
    }
  };

  // 和上面同理符合PromiseA+,2.1.3.1
  const reject = function (value) {
    if (self.info.status === "pending") {
      self.info.status = "onRejected";

      self.info.value = value;

      self.onRejectedArr.forEach((fn) => fn(value));
    }
  };

  fn(resolve, reject);
};

resolve 的附加实现

其实写到这里我们的 resolve 函数还是有一些功能没有实现的, 我们知道 调用 resolve(x), x 的值有好几种情况,如下

  • 如果 x 是 Promise 实例本身,则抛出错误
  • 如果 x 是一个 Promise 对象,那么 then 函数的执行取决这个 x 的状态,如果 x 也调用 resolve(y),其中 y 也是一个 promise 对象.那么 then 函数的执行取决于这个 promise 对象,依次类推,直到最后一个 promise 状态更改
  • 如果 x 是一个 thenable 对象,就是一个对象包含 then 这个属性,或者是一个函数包含一个 then 的静态方法,那么直接执行 then 函数
  • 如果 x 是一个普通值,直接变成 onFulfilled 状态,执行后面的 then 函数

思考

  • 网上实现的大部分 resolve 都是我上面的代码,但是根据规范,resolve 函数里面应该是要判断上面几点的,所以我们上面写的代码是有误的
  • 还有一个问题是,我们需要在 resolve 函数里面判断 x 是不是实例的本身,但是正常的 resolve 函数我们经常是传入一个参数,所以中间肯定是有一个中间函数的,看下面的代码
const PromiseCopy = function (fn) {
  this.info = {
    status"pending",
    value"",
  };

  const self = this;

  self.onFulfilledArr = []; // then函数里面的第一个回调函数的集合
  self.onRejectedArr = []; // then函数里面的第二个回调函数的集合

  // _resolve 是我们经常调用的resolve
  // 但是真正实现的应该是里面的resolve
  const _resolve = function (value) {
    // 这个函数得改变一下
    // PromiseCopy一旦被实例化,那么self就是实例本身了
    resolve(self, value);
  };

  // 此时我们就可以在resolve进行判断了
  const resolve = function (promise, value) {
    let ifexec = false;

    // 首先判断value是不是promise本身
    if (value === promise) {
      // 一定要用TypeError写 不然promises-aplus-tests跑不通
      // 切记这是第一个坑,promises-aplus-tests只认TypeError这种错误形式
      reject(new TypeError("A promise cannot be onFulfilled with itself."));
    }

    // value是一个thenable对象
    // 这个要Object.prototype.toString.call(value) === "[object Object]"判断
    // 不然resolve([])有问题,不知道是不是我实现问题
    if (
      value &&
      (Object.prototype.toString.call(value) === "[object Object]" ||
        typeof value === "function")
    ) {
      // var promise1 = Promise.resolve(dump).then(function () {
      //   return {
      //     then: (resolve, reject) => {
      //       setTimeout(() => {
      //         resolve({
      //           then: (resolve, reject) => {
      //             resolve("aa111a");
      //             throw "other";
      //           },
      //         });
      //       });
      //     },
      //   };
      // });

      // promise1.then(
      //   (res) => {
      //     console.log(res === "aa111a");
      //     console.log("aaa");
      //   },
      //   (res) => {
      //     console.log(res);
      //     console.log("error");
      //   }
      // );

      // 这里的try--catch一定要加 ,不然会promises-aplus-tests会一直报错,这是第三个大坑
      // 因为promises-aplus-test测试里面有这一条的
      // 看上面注释例子

      try {
        // 拿到then函数

        const then = value.then;

        // 如果then是一个函数则执行这个函数
        if (typeof then === "function") {
          // 为什么要.call(value, x, y) 你们可以自己试一下原生的Promise在这种情况下this指向的就是value,所以要绑定
          // 因为then我们已经拿出来了then = value.then,直接调用then(),this就指向的window
          // 为什么后面还需要绑定两个函数了
          // 根据原生的Promise可知,thenable中的then函数可以接受两个函数resolve,reject
          // 只有手动调用了resolve和reject才会执行后面的.then操作,具体大家自己操作下

          then.call(
            value,
            function (value) {
              if (ifexec) {
                return;
              }

              // ifexec这个一定要加,不然也会报200ms错误,第四个大坑
              // 目的是为了不让多次执行,语言无法表达看下面的例子
              // var promise1 = Promise.resolve(dump).then(function () {
              //   return {
              //     then: (resolve, reject) => {
              //       resolve("aa111a");
              //       resolve("aa111a");
              //     },
              //   };
              // });
              ifexec = true;
              resolve(promise, value);
            },
            function (value) {
              if (ifexec) {
                return;
              }

              ifexec = true;
              reject(value);
            }
          );

          return;
        }
      } catch (e) {
        if (ifexec) {
          return;
        }

        ifexec = true;
        reject(e);
      }
    }

    // 下面这一点非常的重要,是async,await 和一些插件比如saga的核心
    // 就是如果x是一个promise对象,那么then的执行取决于x的状态
    // 还有这一个判断一定要放在这里,不要和上面的换 不然promises-aplus-tests会报一个超过200ms的错误,切记这是第二个坑
    if (value && value instanceof PromiseCopy && value.then === promise.then) {
      // 将promise的onFulfilledArr给到value
      // 但是还没有那么简单我们要明白两点
      // 如果value这个promise已经不是pendding,我们给了他也没有用,所以需要直接调用
      if (value.info.status === "pending") {
        value.onFulfilledArr = self.onFulfilledArr;
        value.onRejectedArr = self.onRejectedArr;
      }

      // 如果value状态是onFulfilled
      if (value.info.status === "onRejected") {
        self.info.value = value.info.value;

        self.onRejectedArr.forEach((fn) => fn(value.info.value));
      }

      // 如果value状态是reject
      if (value.info.status === "onFulfilled") {
        self.info.value = value.info.value;

        self.onFulfilledArr.forEach((fn) => fn(value.info.value));
      }

      return;
    }

    // 如果是一个普通的值
    // 加这个判断是为了表示,只有在pendding状态下才会去执行
    // 状态已经变成onFulfilled之后就不能再去改变了
    // 符合PromiseA+中的2.1.2.1
    if (self.info.status === "pending") {
      self.info.status = "onFulfilled";

      self.info.value = value;

      self.onFulfilledArr.forEach((fn) => fn(value));
    }
  };

  // 和上面同理符合PromiseA+,2.1.3.1
  // reject没有resolve那么多规则,比较简单
  const reject = function (value) {
    if (self.info.status === "pending") {
      self.info.status = "onRejected";

      self.info.value = value;

      self.onRejectedArr.forEach((fn) => fn(value));
    }
  };

  // 此时fn调用的是_reoslve
  // 这个try catch主要是实现promiseCopy.prototype.catch
  try {
    fn(_resolve, reject);
  } catch (e) {
    setTimeout(() => {
      self.onRejectedArr.forEach((fn) => fn(e));
    });
  }
};

then 的实现

我们上面介绍的是 promise 的 resolve 用法,promise 还有一个基本用法就是后面接 then,因为是.then 所以我们想到的是这个 then 方法挂在到原型上的,那么 new PromiseCopy 的时候就可以得到这个 then。then 里面是两个函数,一个是 onFulfilled 后执行的回调,一个是 onRejected 后执行的回调。现在的问题是他是怎么做到 then 里面的函数是在 resolve 和 reject 后执行的?这种推迟执行或者说在某种情况下去执行我们想到的就是观察者模式了。下面用代码把上面的话实现一遍,在代码里面会写详细一点的注释。

PromiseCopy.prototype.then = function (onFulfilled, onRejected) {
  const self = this;
  // 这里要判断下,如果PromiseCopy是resolve了那么就直接执行onFulfilled
  if (self.info.status === "onFulfilled") {
    setTimeout(() => {
      onFulfilled(self.info.value);
    });
  }

  if (self.info.status === "onRejected") {
    setTimeout(() => {
      onRejected(self.info.value);
    });
  }

  // 根据PromiseA+中的2.2.1.1和2.2.1.2,onFulfilled和onRejected必须是函数,不然就会被忽略
  if (typeof onFulfilled === "function") {
    self.onFulfilledArr.push(() => {
      setTimeout(() => {
        onFulfilled(self.info.value);
      });
    });
  }

  if (typeof onRejected === "function") {
    self.onRejectedArr.push(() => {
      setTimeout(() => {
        onRejected(self.info.value);
      });
    });
  }

  // 根据PromiseA+ 2.2.7规范 then函数必须返回一个promise对象
  return new PromiseCopy((resolve, reject) => {});
};

then 的额外实现

上面实现的 then 也是一个简单的用法,不过根据 PromiseA+的规范这个 then 函数还有几个点没有实现,看代码解释

promise2 = promise1.then(onFulfilled, onRejected);

promise2.then(onFulfilled, onRejected);
  • promise1.then 中的 onFulfilled,onRejected 函数如果返回一个 x,那么当作[[Resolve]](promise2, x)来处理,就跟上面的 resolve 一样处理,注意如果函数什么都没有返回,就是返回的 undefined
  • promise1.then 函数中的两个回调函数只要有一个报错,那么直接调用 promise2.then 函数中的错误回调
  • 如果 promise1.then 的第一个回调不是函数,并且 promise1 调用的是 resolve,那么 promise2.then 的第一个回调参数是 promise1 中 resolve 函数的抛出值
  • 同理,如果 promise1.then 第二个回调不是函数,并且 promise1 调用的是 reject,那么 promise2.then 中的错误回调就会执行

思考

如果像上面这么说的话,这个新抛出来的 promise 何时调用这个 resolve 或者 reject 是一个关键, 并且这个抛出的 promise 的执行还得看 onFulfilled 和 onRejected 返回值,这一点当时写 promise 的时候想了很久,不知道如何组织,后来实在想不出来,看了下网上很多文章,发现这些逻辑都是在 PromiseCopy 主体里面实现的。

return new PromiseCopy((resolve, reject) => {});

then 实现加强版

PromiseCopy.prototype.then = function (onFulfilled, onRejected) {
  const self = this;

  // 这个一定要这么写目的为了让值传递
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (val) => val;

  // 这个一定要这么写,一定要抛出一个错throw err
  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : (err) => {
          throw err;
        };

  const newPromise = new PromiseCopy((resolve, reject) => {
    if (self.info.status === "onFulfilled") {
      setTimeout(() => {
        try {
          // 如果onFulfilled不是一个函数resolve--self.info.value

          let value = self.info.value;

          // 这个注释不要,留着只是为了记录当时的思路
          // 这个加判断是为了防止then函数逇回调不是一个函数,,是一个字符串
          //   if (typeof onFulfilled === "function") {
          //     value = onFulfilled(value);
          //   }

          value = onFulfilled(value);

          // 这里要做一个[[Resolve]](promise2, x)处理了
          // 因为resolve里面直接做了,所以直接调用,和网上的一些实现有点不一样
          // 他们是提取了一个resolvePromise函数调用,我是直接调用了resolve
          resolve(value);
        } catch (e) {
          reject(e);
        }
      });
    }

    // 注意这里根据上面可知onFulfilled,onRejected抛出的值都要经过[[Resolve]](promise2, x)
    // 这和resolve,reject不一样,promise中resolve才走[[Resolve]](promise2, x),reject不走
    if (self.info.status === "onRejected") {
      setTimeout(() => {
        try {
          let { value } = self.info;

          value = onRejected(self.info.value);

          resolve(value);
        } catch (e) {
          reject(e);
        }
      });
    }

    // 如果是pending状态也需要push
    if (self.info.status === "pending") {
      self.onFulfilledArr.push((data) => {
        setTimeout(() => {
          try {
            let value = data;
            value = onFulfilled(value);

            resolve(value);
          } catch (e) {
            reject(e);
          }
        });
      });

      self.onRejectedArr.push((data) => {
        setTimeout(() => {
          try {
            let value = data;

            value = onRejected(data);

            resolve(value);
          } catch (e) {
            reject(e);
          }
        });
      });
    }
  });

  return newPromise;
};

小结

到这里 promise 的主体实现已经完成了,下面是测试结果

Promise 其他静态方法

Promise.resolve

PromiseCopy.resolve = function (data) {
  return new PromiseCopy((resolve, reject) => {
    resolve(data);
  });
};

reject

Promise.reject = function (reason) {
  return new Promise((resolve, reject) => {
    reject(reason);
  });
};

Promise.all

这个方法有几个特点如下

  • 该方法接受一个数组,数组每一个元素都是一个 promise 对象
  • 只有所有 promise 都是 onFulfilled 的时候才会执行 then 回调,并且结果顺序和数组的一致
  • 如果其中一个 promise 发生了 reject 那么就会返回这个值
PromiseCopy.all = function (data) {
  let count = 0// 记录调用次数
  let total = data.length;
  let result = [];

  return new PromiseCopy((resolve, reject) => {
    for (let i = 0; i < total; i++) {
      data[i].then(
        (res) => {
          result.push(res);

          ++count;

          if (count === totla) {
            resolve(result);
          }
        },
        (res) => {
          return reject(res);
        }
      );
    }
  });
};

Promise.race

这个方法也有以下几个特点

  • 这个方法也是接受数组,数组的元素是 promise
  • 他只返回最快的那一个 promise 的值
  • 就算有错误也会返回最快那一个 promise 的值
PromiseCopy.race = function (data) {
  const total = data.length;

  return new PromiseCopy((resolve, reject) => {
    for (let i = 0; i < total; i++) {
      data[i].then(
        (res) => {
          resolve(res);
        },
        (res) => {
          return reject(res);
        }
      );
    }
  });
};

catch 方法

PromiseCopy.prototype.catch = function (onRejected) {
  // 能到catch里面来的一定是走的reject的
  // 而且状态一定是pendding
  const self = this;
  const newPromise = new PromiseCopy((resolve, reject) => {
    if (self.info.status === "onRejected") {
      try {
        setTimeout(() => {
          let { value } = self.info;

          if (typeof onRejected === "function") {
            value = onRejected(self.info.value);
          }

          resolve(value);
        });
      } catch (e) {
        rejetc(e);
      }
    }

    if (self.info.status === "pending") {
      self.onRejectedArr.push((data) => {
        setTimeout(() => {
          try {
            let value = data;

            if (typeof onRejected === "function") {
              value = onRejected(data);
            }

            resolve(value);
          } catch (e) {
            reject(e);
          }
        });
      });
    }
  });

  return newPromise;
};

// 后来发现catch有一个简单的实现方法
// 没有删除上面就是为了记录思路过程
Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
};

deferred

这个是 Promise 提供的一个快捷使用,自己实现 promise 的时候一定要加,不然 promises-aplus-tests promise.js 跑不过

PromiseCopy.defer = PromiseCopy.deferred = function () {
  let dfd = {};
  dfd.promise = new PromiseCopy((resolve, reject) => {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
};

源码

promise 源码已经上传到个人 github ( https://github.com/yizhengfeng-jj/promise 欢迎 star)

参考文章

解读Promise内部实现原理

Promise 源码分析

小邵教你玩转promise源码


推荐阅读  点击标题可跳转

1、带你写出符合 Promise/A+ 规范 Promise 的源码

2、你真的懂 Promise 吗?

3、这一次,彻底弄懂 Promise 原理


觉得本文对你有帮助?请分享给更多人

关注「前端大全」加星标,提升前端技能

点赞和在看就是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存