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

代码在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 是一个比拟乏味的过程,下面如果有什么问题的,欢送留言多多交换