最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从0到1实现A+规范Promise(上篇)

    正文概述 掘金(二营长敲代码)   2021-03-24   624

        Promise在日常开发中使用非常广泛,得益于其灵活的异步操作处理机制,我们对异步操作(尤其是具有依赖关系的异步操作)的处理大为简化。而了解其底层运行机制将有助于我们更灵活的使用Promise。本文旨在记录/总结我实现Promise的过程并分享思路。其中上篇介绍Promise基本功能与then方法的实现,下篇介绍其他实例方法与静态方法的实现。

    在开始之前首先要说明几点

    1. 本文适合对Promise有一定了解且有使用经验的小伙伴食用。关于Promise的基本使用我在前面有过介绍。
    2. 我们本次实现的Promise是完全按照PromiseA+规范来实现的。不了解PromiseA+规范的小伙伴可以先参考一下。ES6中的Promise就采用了该规范。
    3. 我们的总体思路是,首先回顾在日常开发中,Promise的某个功能点是如何使用的。进一步思考如何实现,做到有的放矢。

    源码地址,欢迎star?

    一. 搭建初始结构

        我们首先来搭建初始结构,在使用promise时,首先要在其构造函数传入executor函数,我们称之为执行器函数。执行器函数接收两个参数resolve,reject,这同样是两个函数,我们用它们来改变Promise的状态和结果。执行器函数同步执行,若执行过程中抛出错误,则promise立即变为失败状态。 而promise的状态有三种,分别是pending(等待),fulfilled(成功),rejected(失败)。状态只能从等待转变为成功/失败,且只能改变一次。

    因此实现思路也就有了

    • Promise构造函数中需要传入一个executor函数,默认立即同步执行,若执行中抛出错误,立即执行reject()。
    • Promise内部提供两个方法 resolve(成功)、reject(失败) ,可以更改Promise的状态和结果。
    • Promise有3个状态: (等待pending、成功fulfilled、失败rejected)。

    我们按照上述搭建一下初始结构

    // 声明Promise的三种状态
    const Pending = "pending"; // 等待
    const Fulfilled = "fulfilled"; // 成功
    const Reject = "rejected"; // 失败
    
    function Promise(executor) {
      // 初始时为等待状态
      this.PromiseState = Pending;
      // 存储promise的结果
      this.PromiseResult = null;
      // 存储成功/失败的回调函数 后面会介绍如何使用
      this.callbacks = [];
      const resolve = () => {};
      const reject = () => {};
      // 同步执行执行器函数 若抛出错误 则执行reject()
      try {
        executor(resolve, reject);
      } catch (e) {
        reject(e);
      }
    }
    

    二.实现resolve/reject

    我们知道resolve/reject的职责有两点

    • 改变Promise的状态
    • 将传入方法的值设置为Promise的结果。

    我们据此来实现

     const resolve = (data) => {
     // 要注意 Promise的状态只能修改一次 因此一旦发现Promise的状态已经改变 就不再继续向下执行
        if (this.PromiseState !== Pending) return;
        // 修改状态
        this.PromiseState = Fulfilled;
        // 设置结果值
        this.PromiseResult = data;
       
      };
      const reject = (data) => {
        if (this.PromiseState !== Pending) return;
        this.PromiseState = Reject;
        this.PromiseResult = data;
      };
    

    三.添加实例方法 then

    then方法的功能以及实现比较复杂,我们分几个步骤进行。

    1.添加对回调函数的处理

        首先,我们使用then方法时,通常会传入两个回调函数(当然也可以只指定其中一个或都不指定),分别是Promise成功/失败后的回调。 而then的职责就是根据Promise的状态执行对应的回调函数(这样说其实是不准确的,后面会解释)。

    Promise.prototype.then = function(onResolved, onRejected){
        //根据promise的状态 执行相应的回调函数
        if(this.PromiseState === Fulfilled){
            // 调用回调函数时,传入promise的结果,也即调用resolve/reject时传入的参数
            onResolved(this.PromiseResult);
        }
        if(this.PromiseState === Reject){
            onRejected(this.PromiseResult);
        }
    }
    
    2.完善then方法,添加对异步任务的回调处理。

        目前实现的then方法存在缺陷。它只能解决同步调用resolve/reject的情况。我们来捋一下。

        我们知道resolve/reject方法会改变Promise的状态,因此当resolve/reject同步执行(即执行器函数中进行的是同步操作)时,会导致执行then方法时resolve/reject已经执行完毕,即此时Promise一定已经改变,可以顺利执行then指定的回调。

        但当resolve/rejetc异步调用,换句话说,我们在执行器函数中进行的是异步操作。这会导致resolve/rejetc的调用操作会进入任务队列。因此当执行then方法时,Promise的状态没有改变。仍然是'pending'。而我们知道,成功/失败的回调函数是一定要等到Promise的状态改变后再执行的。怎么办呢? 这时候,我们在Promise构造函数中声明的callbacks数组就排上了用场。我们可以在then方法中判断,当状态为'pending'时,将成功/失败的回调推入该数组中。等将来Promise状态改变时(也就是resolve/reject调用时)再取出来调用。因此我们也要进一步完善resolve/reject方法,使其能够在callbacks中存有回调时,循环调用。 为什么callbacks是个数组呢,这样可以允许我们指定多个then方法。

    首先完善then方法

    Promise.prototype.then = function(onResolved, onRejected){
        //根据promise的状态 执行相应的回调函数
        if(this.PromiseState === Fulfilled){
            // 调用回调函数时,传入promise的结果,也即调用resolve/reject时传入的参数
            onResolved(this.PromiseResult);
        }
        if(this.PromiseState === Reject){
            onRejected(this.PromiseResult);
        }
        //pending状态时,暂存回调函数
        if(this.PromiseState === Pending){
            this.callbacks.push({
                onResolved,
                onRejected
            })
        }
    }
    

    完善reject/resolve

    const resolve = (data) => {
        if (this.PromiseState !== Pending) return;
        this.PromiseState = Fulfilled;
        this.PromiseResult = data;
        // 判断是否有暂存的回调函数 有则循环调用
        if (this.callbacks.length > 0) {
          this.callbacks.forEach((cb) => cb.onResolved(data));
        }
      };
    const reject = (data) => {
        if (this.PromiseState !== Pending) return;
        this.PromiseState = Reject;
        this.PromiseResult = data;
          if (this.callbacks.length) {
            this.callbacks.forEach((cb) => cb.onRejected(data));
          }
      };
    
    3.继续完善then。

        在A+规范中约定,Promise的then方法会返回一个Promise,且该Promise的状态由then方法中指定的回调函数的返回结果决定。经过上面的讨论我们知道,then中的回调执行要分同步与异步两种情况。同样,我们这里也分开讨论。

        同步的情况比较简单,由于调用then时,Promise的状态已经改变,因此我们只需调用相应的回调函数并拿到其执行结果。根据结果来决定then方法返回的Promise的状态即可。

    Promise.prototype.then = function (onResolved, onRejected) {
      // 创建一个新的Promise 最后返回它
      let promise = new Promise((resolve, reject) => {
        if (this.PromiseState === Fulfilled) {
          try {
            // 拿到回调的执行结果
            let res = onResolved(this.PromiseResult);
            // 若结果是promise 则then的状态和结果由该promise决定
            // 这里的判断条件并不严苛 后面会继续完善
            if (res instanceof Promise) {
              // 若返回结果是Promise 则一定可以执行then方法
              // 我们从then方法中获取回调返回的Promise的状态和结果,将其作为then的状态和结果
              res.then(
                (v) => {
                  resolve(v);
                },
                (r) => {
                  reject(r);
                }
              );
            } else {
              // 若是普通值则直接返回成功的promise并将该值作为结果值
              resolve(res);
            }
            // 执行过程中抛出错误则直接返回失败的promise
          } catch (e) {
            reject(e);
          }
        } else if (this.PromiseState === Reject) {
          try {
            let res = onRejected(this.PromiseResult);
            if (res instanceof Promise) {
              res.then(
                (v) => {
                  resolve(v);
                },
                (r) => {
                  reject(r);
                }
              );
            } else {
              resolve(res);
            }
          } catch (e) {
            reject(e);
          }
        }
      });
      // 返回该promise
      return promise;
    };
    

        接下来讨论异步修改Promise状态时,then返回的Promise的状态和结果问题。我们知道异步修改状态时,成功/失败的回调函数不是在then方法中直接执行。而是会暂存起来,在resolve/reject中执行。而这两个方法是在Promise构造函数中声明的。我们如何才能在构造函数中改变实例方法then的状态呢?这就需要在then暂存回调函数的操作中为回调函数绑定执行上下文

    // 我们把根据回调结果决定then的返回状态的操作先简单封装一下 后面会继续完善
    function resolvePromise(result, resolve, reject) {
      try {
        if (result instanceof Promise) {
          result.then(
            (v) => {
              resolve(v);
            },
            (r) => {
              reject(r);
            }
          );
        } else {
          // 若是普通值则直接返回成功的Promise并将该值作为结果值
          resolve(result);
        }
      } catch (e) {
        reject(e);
      }
    }
    Promise.prototype.then = function (onResolved, onRejected) {
      let promise = new Promise((resolve, reject) => {
        if (this.PromiseState === Fulfilled) {
          try {
            let res = onResolved(this.PromiseResult)
            resolvePromise(res, resolve, reject);
          } catch (e) {
            reject(e);
          }
        } else if (this.PromiseState === Reject) {
          try {
            let res = onRejected(this.PromiseResult);
            resolvePromise(res, resolve, reject);
          } catch (e) {
            reject(e);
          }
        } else {
          this.callbacks.push({
          // 对异步操作的回调处理 其行为与上面的同步操作的回调处理行为一致 只是要绑定上下文 否则将来执行时会丢失this
            onResolved: function () {
              try {
                let res = onResolved(this.PromiseResult);
                resolvePromise(res, resolve, reject);
              } catch (e) {
                reject(e);
              }
            }.bind(this), // 绑定上下文
            onRejected: function () {
              try {
                let res = onRejected(this.PromiseResult);
                resolvePromise(res, resolve, reject);
              } catch (e) {
                reject(e);
              }
            }.bind(this),
          });
        }
      });
      // 返回该promise
      return promise;
    };
    

    四.添加catch方法。

        catch方法主要用来捕获错误,而该方法的一大特性是能够捕获穿透的异常。也就是能捕获在任一阶段抛出的异常。因此我们要解决两个问题

    1.捕获错误并执行错误回调。

        这一点比较好实现,catch方法实质上就是特殊的then方法,我们只需要指定失败的回调函数即可。

    2.实现异常穿透。

        我们首先要了解穿透的意义是什么,即当链式调用的某个节点抛出了异常,但没指定相应的失败回调,则该错误信息会一直向下传递,直到被catch方法捕获。 而实现穿透的关键在于,如何实现在没指定回调函数的情况下,将状态和结果向下传递。 因此我们要指定回调的默认行为

        默认行为的职责就是将错误传递下去。因为既然要穿透,说明我们没有为前面的错误指定回调。因此才要将错误向下传递,让后面的错误回调来捕获到该错误。因此默认回调的行为也就是将错误信息传递下去。如何传递呢?试想一下 既然要调用错误的回调,说明上一个Promise对象状态为失败了。因此默认回调就是要让它一直错下去!怎么办? 使用throw抛出错误

        我们知道,在then的链式调用过程中,then返回的Promise的状态和结果是由then的回调的返回结果决定的。因此若在默认的失败回调中抛出了错误,则会立即被trycatch捕获到。因此当前的then的返回结果会立即变为失败的Promise且结果是抛出的错误信息。再进一步,由于当前then的返回了失败的Promise,因此下个then一定会执行其失败回调。若下个then指定了失败回调,则前面的错误就被捕获到了。若仍然没指定失败回调,则又会执行默认的失败回调。由此就达到了异常穿透的效果。假设我们在then的链式调用过程中一直没指定失败回调,则最终抛出的错误就会被catch方法捕获。

        同理,成功的状态和结果也可以传递,也就是我们在then的链式调用过程中,即使没有为中间的某个then指定回调函数也不会中断链式调用。其状态和结果会继续向下传递。

    接下来实现catch方法和指定then方法的默认回调行为。

    Promise.prototype.catch = function (onRejected) {
      // 直接调用then,不传成功的回调
      return this.then(undefined, onRejected);
    };
    
    Promise.prototype.then = function (onResolved, onRejected) {
      if (typeof onRejected !== "function") {
        onRejected = (reason) => {
        // 抛出异常这将使得下个then继续执行失败回调
          throw reason;
        };
      }
      if (typeof onResolved !== "function") {
      // 返会成功信息 下个then会执行成功回调
        onResolved = (value) => value;
      }
      ......
    };
    

    五.异步执行回调

        这里要说明一点,我们一般认为Promise的then方法是异步执行的,而且在日常使用中Promise的then方法的行为似乎也印证了这一点。但实际上真正异步执行的是then方法指定的回调函数。可是then方法的职责不就是根据Promise的状态来执行相应的回调吗?事实上经过前面的then方法的实现我们已经知道,then方法本身是同步执行的,当执行then时,若Promise状态已经改变,则会执行回调。若未改变则会将回调暂存。由此可见回调的执行不一定是在then方法中,因此我们说前面对then方法职责的阐述是有有缺陷的。因此要实现回调的异步执行我们不能从then方法下手,而是应对回调函数本身动手脚。实现异步执行的方式有很多,这里就用定时器实现。

    //这里就以执行成功的回调为例,我们只需包一层定时器即可。
    ......
     if (this.PromiseState === Fulfilled) {
          setTimeout(() => {
            try {
              let x = onResolved(this.PromiseResult);
              resolvePromise(res, resolve, reject);
            } catch (e) {
              reject(e);
            }
          });
        }
     ......
    

    六.细节问题

    至此Promise的基本功能已经完成,接下来完善几个细节问题

    1.then方法中成功/失败的回调的返回值问题。具体如下

        当回调的返回值与当前的then方法的返回值引用了同一个promise对象时,会造成死循环,因此应抛出错误。 接下来就是具体判断返回值是不是Promise。我们之前用的instanceof方法不能最准确的判断。由于该回调函数的返回值直接决定了then的状态和结构,因此我们要严格判断它是不是Promise。按照PromiseA+规范,只有当返回值的类型是对象或函数,存在then属性,且then属性是函数时这样才能保证返回值它是Promise。同时还要保证,若resolve/reject同时被调用或被调用多次,只取第一次,其他调用会被忽略。 当resolve/reject返回的仍然是Promise,则递归解析直到为普通值。这块逻辑具体可以参考A+规范文档中对该部分的阐述。

    下面来完善resolvePromise方法

    function resolvePromise(promise, res, resolve, reject) {
      // 1.回调的返回值和then的返回值不能引用同一个对象 可能造成死循环
      if (promise === res) {
        return reject(new TypeError("不能引用同一个对象"));
      }
      // 该变量为已经调用回调的标志,避免多次调用。
      let called;
      // 2.res是对象或者函数,说明有可能是promise
      if ((typeof res === "object" && res != null) || typeof res === "function") {
        try {
          let then = res.then; // 获取其then属性
          // 存在then属性,且是函数类型,则可以断定是promise
          if (typeof then === "function") {
          // 调用then并绑定上下文
            then.call(
              res,
              (y) => {
                // 避免多次调用
                if (called) return;
                called = true;
                // 若返回值仍是promise 则递归解析直到为普通值
                resolvePromise(promise, y, resolve, reject);
              },
              (r) => {
                if (called) return;
                called = true;
                reject(r);
              }
            );
          } else {
            resolve(res);
          }
        } catch (e) {
        // 若取then或执行then的过程中出错,直接返回失败。
          if (called) return;
          called = true;
          reject(e);
        }
      } else {
        resolve(res);
      }
    }
    

    由于修改了resolvePromise,因此调用该方法的地方也要做出调整。

    ......
    if (this.PromiseState === Fulfilled) {
          setTimeout(() => {
            try {
              let res = onResolved(this.PromiseResult);
              //将当前then方法即将返回的Promise传入
              resolvePromise(promise, res, resolve, reject);
            } catch (e) {
              reject(e);
            }
          });
        }
    ......
    
    2 resolve/reject中传入的仍是Promise。

        上面分析过程中有类似的情况,解决办法就是递归解析直到为普通值。

    ......
    let resolve = (value) => {
            // 增加判断如果resolve传入的是promise的判断
            // 这里无需进行像上面那样苛刻的判断,我们要的只是他的返回值
            if (value instanceof Promise) { // 递归解析直到为普通值为止
                  value.then(resolve, reject)
                  return
              }
          }
    ......
    

    七.测试

    至此Promise的基本功能已经实现。

    我们可以用promises-aplus-tests这款插件来测试我们写的promise符不符合A+规范。 分为三步

    • 1 全局安装 npm i -g promises-aplus-tests

    • 2 在我们写的promise.js文件中配置脚本

    ......
    Promise.defer = Promise.deferred = function () {
          let dfd = {}
          dfd.promise = new Promise((resolve, reject) => {
              dfd.resolve = resolve
              dfd.reject = reject
          })
          return dfd
      }
    module.exports = Promise;
    
    • 3 运行文件测试 promises-aplus-tests promise.js

    只要通过全部测试,则说明我们写的Promise是符合PromiseA+规范的,如下图所示。

    从0到1实现A+规范Promise(上篇)

    以上就是符合A+规范的Promise的实现过程,在下篇中将继续完成Promise的其他实例方法和静态方法。

    参考:github.com/Tie-Dan/Pro…


    起源地下载网 » 从0到1实现A+规范Promise(上篇)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元