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

发表于 3年以前  | 总阅读数:390 次

前言

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内部实现原理

Promise 源码分析

小邵教你玩转promise源码

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/pELnbmQkpsz2LmIQBnSeQQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237299次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8141次阅读
 目录