『你写的Promise, 是完美的吗?』

66 阅读13分钟

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

代码在github

《手写 Promise》是一个经典的问题,基本上大家上手都可以按照自己的理解,写出来一个 promise, 有一天个朋友问我,"手写 Promise 要写到什么程度才是合格的 ?", 这也引起了我的兴趣和思考, "怎么样的 Promise ,才是完美的呢 ? "

完美的 Promise

第一个问题就是怎么样才算是一个完美的 Promise 呢, 其实这个问题也不难,实现一个和原生 Promise "相同"的 Promsie,不就是完美的了, 那么第二个问题也就来了,原生的 Promise 是按照什么标准来实现的呢, 查阅了资料之后知道是按照 [Promises/A+] (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 , 里面目前包含872个测试用例,用于测试 Promise 是否标准

正文开始

首先说明下这边是按照已完成的代码对实现 promise 进行介绍代码在这里, 这里使用的是最终版本,里面注释大致标明了实现的规则编号,其实整体来说经过了很多修改,如果要看整个便携过程,可以commit history, 关注promise_2.jspromise.js 两个文件

编写的关键点

整体的实现思路主要就是上面的规范了,当然我们也不是说逐条进行实现,而是对规范进行分类,统一去实现:

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 是一个比较有趣的过程,上面如果有什么问题的,欢迎留言多多交流