关于javascript:『你写的Promise-是完美的吗』

38次阅读

共计 10667 个字符,预计需要花费 27 分钟才能阅读完成。

欢送来这里 前端杂谈, 聊聊前端

代码在 github

《手写 Promise》是一个经典的问题,基本上大家上手都能够依照本人的了解,写进去一个 promise, 有一天个敌人问我,” 手写 Promise 要写到什么水平才是合格的 ?”, 这也引起了我的趣味和思考, “ 怎么样的 Promise,才是完满的呢 ? “

完满的 Promise

第一个问题就是怎么样才算是一个完满的 Promise 呢, 其实这个问题也不难,实现一个和原生 Promise “ 雷同 ” 的 Promsie,不就是完满的了, 那么第二个问题也就来了,原生的 Promise 是依照什么规范来实现的呢, 查阅了材料之后晓得是依照 [Promises/A+] (https://promisesaplus.com/)规范来实现的,具体的实现在 ECMA – sec-promise-objects 上有记录, 当初规范有了,咱们就能够来实现一个 ” 完满的 Promise” 了

Promises/A+

接下来咱们来看看 Promises/A+ 规范说了啥, 次要是两局部,一个是名词定义,一个是规范形容,其中规范形容由三个局部组成, 接下来咱们简略介绍下:

Terminology

这部分是名词定义,次要是形容了各个名词在规范中的定义

  • promise: 是具备 then 行为符合规范的办法的 objectfunction, 这里须要留神的是不是 functionthen, 是 function 中有then 办法
  • thenable: 是定义 then 办法的 object函数 ,这个和下面promise 的区别在于 then 是一个函数,不肯定须要符合规范行为
  • value: 是任何非法的 javascript 值,包含 undefinedthenablepromise , 这里的value 蕴含了 thenablepromise, 联合上面的标准, 会发现是一个可嵌套的关系
  • exception: 是一个通过throw 关键词抛出来的值
  • reason: 示意一个 promise 状态是rejected 的起因

Requirements

这部分是规范的定义, 分为以下三个局部

Promise States

一个 promise 必须是以下三种状态之一

  • pending

    • 能够转变成 fulfilled 或者 rejected 状态
  • fulfilled

    • 须要存在一个value
  • rejected

    • 须要存在一个reason

当状态是 fulfilled 或者 rejected 时,状态不能够再变动成其余状态,而valuereason 也不能够再变动

The then Method

这部分定义了 promisethen 办法的行为,then 办法是用来拜访 promise 状态变成fulfilled 或者 rejectedvalue 或者reason 的,then 有两个参数,如下:

promise.then(onFulfilled,onRejected)

  • onFulfilled / onRejected

    • 都是可选参数,如果这两个参数不是函数类型,那么疏忽
    • promise 状态变成fulfilled/rejected 之后被调用,会带上value/reason 作为函数的参数
    • 只会被调用一次
    • 须要在 宏工作 或者 微工作 事件循环中实现。注: 这里对于执行机会的形容比拟乏味,能够看看文档 2.2.4
    • 两个函数须要被绑定在 global this 上运行
  • 同一个 Promise 能够被屡次 then 调用, then 中的 onFulfilledonRejected 必须依照then 的调用顺序调用
  • then 函数调用之后须要返回一个 promise , 这也是promise 能够链式调用 then 的根底

    promise2 = promise1.then(onFulfilled,onRejected)

    • 如果 onFulfilled 或者 onRejected 函数返回了值x, 则运行 Promise Resolution Procedure
    • 如果 onFulfilled 或者onRejected 抛出谬误e,则 promise2 的状态是rejected, 并且reasone
    • 如果 onFulfilled 或者 onRejected 不是一个函数, 而且 promise1 的状态曾经确定fulfilled/rejected, 则 promise2

The Promise Resolution Procedure

其实大体的规范局部在 Promise StatesThe then Method 曾经形容完了, 这部分次要规定了一个形象的操作 promise resolution procedure, 用来形容当thenonFulfilled 或者 onRejected 返回值x 时, 须要怎么样去进行操作, 把表达式记为[[Resolve]](promise,x), 这部分也是整个 Promise 实现最简单的局部,咱们一起看看他规定了什么

[[Resolve]](promise,x)

  • promisex 是同一个对象时,promiserejected,reasonTypeError

    const promise = Promise.resolve().then(()=>promise); // TypeError
    
  • 如果 x 是一个 Promise 时,则 promise 的状态要与x 同步
  • 如果 x 是一个 object 或者一个function , 这部分是最简单的

    • 首先要把 x.then 存储在一个两头变量then, 为什么要这么做能够看文档 3.5, 而后依据不同条件进行解决
    • 如果获取 x.then 的时候就抛出谬误e,则promise 状态变成rejected,reasone
    • 如果 then 是一个函数,那么这就是咱们定义外面的 thenable, 这时候绑定 x 为 this 并调用 then,传入 promiseresolvePromiserejectPromise 作为两个参数

      then.call(x, resolvePromise, rejectPromise)

      接下来判断调用的后果

      • 如果 resolvePromise 被调用,valuey, 则调用[[Resolve]](promise,y)
      • 如果 rejectPromise 被调用, reasone, 则 promise 状态变成rejected, reasone
      • 如果 resolvePromiserejectPromise都被调用,则以第一个调用会准,后续的调用都被疏忽
      • 如果调用过程中抛出了谬误e

        • 如果抛出之前 resolvePromise 或者rejectPromise 曾经被调用了,那么就疏忽谬误
        • 后者的话,则 promise 状态变成 rejected,reasone
    • 如果 then 不是一个函数,那么promise 状态变成 fulfilled,valuex
  • 如果 x 不是一个 object 或者function, 则promise 状态变成 fulfilled,valuex

这外面最简单的就是在 resolvePromise 被调用,valuey 这部分, 实现的是thenable 的递归函数

下面就是如何实现一个 ” 完满 ” 的 Promise 的标准了, 总的来说比较复杂的是在The Promise Resolution Procedure 和对于谬误和调用边界的状况,上面咱们将开始入手, 实现一个 ” 完满 ” 的 Promise

如何测试你的 Promise

后面介绍了 Promise/A+标准, 那么如何测试你的实现是齐全实现了标准的呢, 这里Promise/A+ 提供了 [promises-tests
](https://github.com/promises-a…), 外面目前蕴含 872 个测试用例, 用于测试 Promise 是否规范

注释开始

首先阐明下这边是依照已实现的代码对实现 promise 进行介绍代码在这里, 这里应用的是最终版本,外面正文大抵表明了实现的规定编号,其实整体来说通过了很多批改,如果要看整个便携过程,能够 commit history, 关注promise_2.jspromise.js 两个文件

编写的关键点

整体的实现思路次要就是下面的标准了,当然咱们也不是说逐条进行实现,而是对标准进行分类,对立去实现:

  • promise 的状态定义及转变规定和根底运行
  • then 的实现
  • onFulfilled 和 onRejected 的执行及执行机会
  • thenable 的解决
  • promise 和 then 及 thenable 中对于谬误的解决
  • resolvereject 函数的调用次数问题

promise 的状态定义及转变规定和根底运行

const Promise_State = {
  PENDING: "pending",
  FULFILLED: "fulfilled",
  REJECTED: "rejected",
};

class MyPromise {constructor(executerFn) {
    this.state = Promise_State.PENDING;
    this.thenSet = [];
    try {executerFn(this._resolveFn.bind(this), this._rejectedFn.bind(this));
    } catch (e) {this._rejectedFn.call(this, e);
    }
  }
}

在构造函数中初始化状态为 pending, 并且运行传入构造函数的executerFn, 传入resovlePromiserejectePromise 两个参数

而后咱们接下去就要实现 resolvePromise,rejectPromise 这两个函数

  _resolveFn(result) {
    // 2.1.2
    if (this._checkStateCanChange()) {
      this.state = Promise_State.FULFILLED;
      this.result = result;
      this._tryRunThen();}
  }

  _rejectedFn(rejectedReason) {
    //2.1.3
    if (this._checkStateCanChange()) {
      this.state = Promise_State.REJECTED;
      this.rejectedReason = rejectedReason;
      this._tryRunThen();}
  }

  _checkStateCanChange() {
    //2.1.1
    return this.state === Promise_State.PENDING;
  }

这里次要是通过 _checkStateCanChange 判断是否可执行的状态,而后进行状态变更,valuereason 的赋值, 而后尝试运行 then 办法注册的函数

这时候咱们的 promise 曾经能够这么调用了

const p = new MyPromise((resolve,reject)=>{resolve('do resolve');
   // reject('do reject');
});

then 的实现

接下来咱们实现 then 函数,首先有个简略的问题:『then 办法是什么时候执行的?』, 有人会答复,是在 promise 状态变成resolve 或者rejected 的之后执行的,这个乍一看如同没故障,然而其实是有故障的,正确的说法应该是

『then 办法是立刻执行的,then 办法传入的 onFulfilledonRejected 参数会在 promise 状态变成resolve 或者rejected 后执行

咱们先上代码


  then(onFulfilled, onRejected) {const nextThen = [];
    const nextPromise = new MyPromise((resolve, reject) => {nextThen[1] = resolve;
      nextThen[2] = reject;
    });
    nextThen[0] = nextPromise;

    //2.2.6
    this.thenSet.push([onFulfilled, onRejected, nextThen]);
    this._runMicroTask(() => this._tryRunThen());
    return nextThen[0];
  }

代码看起来也挺简略的, 次要逻辑就是结构一个新的 promise, 而后把 onFulfilledonRejected还有新结构的 promise 的 resolvereject 存储到thenSet 汇合中, 而后返回这个新构建的 promise, 这时候咱们的代码曾经能够这样子调用


const p = new MyPromise((resolve,reject)=>{resolve('do resolve');
   // reject('do reject');
});

p.then((value)=>{console.log(`resolve p1 ${value}`);
},(reason)=>{console.log(`reject p1 ${reason}`);
}).then((value)=>console.log(`resolve pp1 ${value}`));

p.then((value)=>{console.log(`resolve p2 ${value}`);
},(reason)=>{console.log(`reject p2 ${reason}`);
});

onFulfilled 和 onRejected 的执行及执行机会

onFulFilledonRejected 会在 promise 状态变成fulfilled 或者 rejected 之后被调用,联合 then 办法被调用的机会, 判断时候状态能够调用须要在两个中央做

  • resolvePromiseresolvePromise 被调用的时候(判断是否有调用了 then 注册了onFulfilledonRejected)
  • then 函数被调用的时候(判断是否 promise 状态曾经变成了fulfilledrejected)

    这两个时机会调用以下函数

    
    _tryRunThen() {if (this.state !== Promise_State.PENDING) {
       //2.2.6
       while (this.thenSet.length) {const thenFn = this.thenSet.shift();
         if (this.state === Promise_State.FULFILLED) {this._runThenFulfilled(thenFn);
         } else if (this.state === Promise_State.REJECTED) {this._runThenRejected(thenFn);
         }
       }
     }
    }
    

    这里会判断时候须要调用 then 注册的函数,而后依据 promise 的状态将 thenSet 中的函数进行对应的调用


  _runThenFulfilled(thenFn) {const onFulfilledFn = thenFn[0];
    const [resolve, reject] = this._runBothOneTimeFunction(thenFn[2][1],
      thenFn[2][2]
    );
    if (!onFulfilledFn || typeOf(onFulfilledFn) !== "Function") {
      // 2.2.73
      resolve(this.result);
    } else {
      this._runThenWrap(
        onFulfilledFn,
        this.result,
        thenFn[2][0],
        resolve,
        reject
      );
    }
  }

_runThenFulfilled_runThenRejected 类似,这里就通过一个进行解说,
首先咱们判断 onFulfilled 或者onRejected 的合法性

  • 如果不非法则不执行,间接将 promise 的 valuereason透传给之前返回给 then 的那个 promise,这个时候相当于then 的 promise 的状态和原来的 promise 的状态雷同
  • 如果非法,则执行onFulfilled 或者 onRejected
  _runThenWrap(onFn, fnVal, prevPromise, resolve, reject) {this._runMicroTask(() => {
        try {const thenResult = onFn(fnVal);
          if (thenResult instanceof MyPromise) {if (prevPromise === thenResult) {
              //2.3.1
              reject(new TypeError());
            } else {
              //2.3.2
              thenResult.then(resolve, reject);
            }
          } else {
            // ... thenable handler code
            // 2.3.3.4
            // 2.3.4
            resolve(thenResult);
          }
        } catch (e) {reject(e);
        }
     });
  }

这里先截取一小段 _runThenWrap, 次要是阐明onFulfilledonRejected的运行,这部分在标准中有这样子的一个形容

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

简略来说就是 onFulfilledonRejected 要在执行上下文外面没有除了 platform code 之后能力执行,这段听起来有点拗口,其实说人话就是咱们常常说的要在 微工作 宏工作
所以咱们这里包装了 _runMicroTask 办法,用于封装这部分执行的逻辑

   _runMicroTask(fn) {
    // 2.2.4
    queueMicrotask(fn);
  }

这里应用 queueMicrotask 作为微工作的实现, 当然这个有兼容性问题,具体能够看 caniuse

实现的办法还有很多,比方setTimeoutsetImmediateMutationObserverprocess.nextTick

而后将 valuereason作为参数执行 onFulfilledonRejected, 而后获取返回值thenResult,接下来就会有几个判断的分支

  • 如果 thenResult 是一个 promise

    • 判断是否和 then 返回的 promise 是雷同的,如果是抛出TypeError
    • 传递 then 返回的 promise 的 resolvereject, 作为 thenResult.thenonFulFilledonRejected 函数
  • 如果 thenResult 不是一个 promise

    • 判断是否是thenable, 这部分咱们在上面进行解说
    • 如果以上判断都不是,那么将thenResult 作为参数,调用resolvePromise

thenable 的解决

thenable应该说是实现外面最简单的一个局部了,首先,咱们要依据定义判断上局部的 thenResult 是否是thenable


   if (typeOf(thenResult) === "Object" ||
      typeOf(thenResult) === "Function"
    ) {
      //2.3.3.1
      const thenFunction = thenResult.then;
      if (typeOf(thenFunction) === "Function") {// is thenable}
    }

能够看到 须要判断是否是一个 Object 或者Function, 而后再判断thenResult.then 是不是个 Function, 那么有人会问,能不能写成这样子

   if ((typeOf(thenResult) === "Object" ||
      typeOf(thenResult) === "Function") && (typeOf(thenResult.then) === 'Function')
    ) {// is thenable}

刚开始我也是这么写的,而后发现测试用例跑不过,最初去看了标准,有这么一段 3.5

简略来说就是为了保障测试和调用的一致性,先把 thenResult.then 进行存储再判断和运行是有必要的,屡次拜访属性可能会返回不同的值

接下去就是 thenable 的解决逻辑了
简略来说thenable 的解决逻辑有两种状况

  • 在 promise 的 then 或者 resolve 中解决 thenable 的状况
  • thenablethen 回调中解决 value 还是thenable 的状况

这里用在 promise 的 thenthenable调用进行讲述:



    _thenableResolve(result, resolve, reject) {
      try {if (result instanceof MyPromise) {
          // 2.3.2
          result.then(resolve, reject);
          return true;
        }

        if (typeOf(result) === "Object" || typeOf(result) === "Function") {
          const thenFn = result.then;
          if (typeOf(thenFn) === "Function") {
            // 2.3.3.3
            thenFn(resolve, reject);
            return true;
          }
        }
      } catch (e) {
        //2.3.3.3.4
        reject(e);
        return true;
      }
    }

    const [resolvePromise, rejectPromise] =
          this._runBothOneTimeFunction((result) => {if (!this._thenableResolve(result, resolve, reject)) {resolve(result);
              }
            },
            (errorReason) => {reject(errorReason);
            }
          );

    try {thenFunction.call(thenResult, resolvePromise, rejectPromise);
    } catch (e) {
      //2.3.3.2
      rejectPromise(e);
    }

这里咱们结构了 resolvePromiserejectPromise, 而后调用 thenFunction, 在函数逻辑中解决实现之后将会调用 resolvePromise 或者 rejectPromise, 这时候如果result 是一个 thenable, 那么就会持续传递上来,直到不是 thenable, 调用resolve 或者reject

咱们要留神的是 promise 的 then 办法和 thenablethen 办法是有不同的中央的

  • promise 的 then 有两个参数,一个是fulfilled, 一个是rejected, 在后面的 promise 状态扭转之后会回调对应的函数
  • thenablethen 也有两个参数,这两个参数是提供给thenable 调用实现进行回调的resolvereject 办法,如果 thenable 的回调值还是一个 thenable, 那么会依照这个逻辑调用上来,直到是一个非thenable, 就会调用离thenable 往上回溯最近的一个 promies 的resolve 或者reject

    到这里,咱们的 promise 曾经能够反对 thenable 的运行

    
    new MyPromise((resolve)=>{
     resolve({then:(onFulfilled,onRejected)=>{console.log('do something');
         onFulfilled('hello');
       }
     })
    }).then((result)=>{
    
     return {then:(onFulfilled,onRejected)=>{onRejected('world');
       }
     }
    });
    
    

promise 和 then 及 thenable 中对于谬误的解决

错误处理指的是在运行过程中呈现的谬误要进行捕捉解决, 基本上应用 try/catch 在捕捉到谬误之后调用 reject 回调,这部分比较简单,能够间接看代码

resolve 和 reject 函数的调用次数问题

一个 promise 中的 resolvereject调用能够说是互斥而且惟一的,就是这两个函数只能有一个被调用, 而且调用一次,这个说起来比较简单, 然而和谬误场景在一起的时候,就会有肯定的复杂性
原本可能是这样子的

if(something true){resolve();
}else {reject();
}

加上谬误场景之后

try{if(something true){resolve();
    throw "some error";
  }else {reject();
  }
}catch(e){reject(e);
}

这时候判断就会有效了, 因而咱们依照通过一个工具类来包装出两个互斥的函数, 来达到目标

  _runBothOneTimeFunction(resolveFn, rejectFn) {
    let isRun = false;

    function getMutuallyExclusiveFn(fn) {return function (val) {if (!isRun) {
          isRun = true;
          fn(val);
        }
      };
    }
    return [getMutuallyExclusiveFn(resolveFn),
      getMutuallyExclusiveFn(rejectFn),
    ];
  }

至此,咱们一个完全符合Promise/A+ 规范的 Promise,就实现了, 残缺代码在这里

等等,是不是少了些什么

有人看到这里会说,这就完了吗?
我常常应用的 catchfinally, 还有静态方法Promise.resolvePromise.rejectPromise.all/race/any/allSettled 办法呢?

其实从规范来说,Promise/A+的规范就是后面讲述的局部,只定义了 then 办法, 而咱们日常应用的其余办法,其实也都是在then 办法下面去派生的,比方catch 办法

 MyPromise.prototype.catch = function (catchFn) {return this.then(null, catchFn);
};

具体的办法其实也实现了,具体能够看 promise_api

最初

最初是想分享下这次这个 promise 编写的过程,从下面的讲述看似很顺利,然而其实在编写的时候,我基本上是简略了过了以下规范,而后依照本人的了解,联合 promises-tests 单元测试用例来编写的,这种开发模式其实就是 TDD(测试驱动开发 (Test-driven development)), 这种开发模式会大大加重发人员编程时候对于边界场景没有笼罩的心智累赘,然而反过来,对于测试用例的便携品质要求就很高了
总体来说这次便携 promise 是一个比拟乏味的过程,下面如果有什么问题的,欢送留言多多交换

正文完
 0