导读

首先,让我们回顾一下Promise的定义,为了偷懒,就直接复制粘贴了:

可以看到,Promise是一个允许异步操作的代理,我更愿意简单点叫它“容器”。

在各大面试题中,手写Promise是一道高频题,笔者自己也遇到了好几次,同时其也涉及到****事件循环的有关知识,为了进一步强化大家对于其原理和事件循环中微任务队列的关系,随将用比较长的篇幅着重介绍这些内容,并包含其PromiseLikeAwaitedtypescript的类型推断。

如果你想直接查看代码,请移步我的

GitHub - zerotower69/handle-promise-TPromise-: achieve myself promise by using queueMicroTask api.

微任务队列

或许你经常听到事件循环这一概念,除此之外还有JS是单线程的这一说法。首先,事件是JS的核心,JS为了实现异步操作,总是使用发布订阅的设计模式,也就是在无法事先确定任务(代码中异步逻辑的部分,通常是函数,也叫回调函数),会使用队列(通常是数组)等结构储存这些任务,待某个未来的时间点事件被触发时依次执行(按进入队列的顺序),这个队列也叫任务队列。在JS这个“单线程”中,异步任务存放在异步任务队列中,而异步任务又分为微任务宏任务,因此,JS单线程中的异步队列实际是微任务队列宏任务队列的统称。但注意,前文的表述中“单线程”并不是说整个JS就是真的一个线程,在整个JS的宿主环境中(分别为web浏览器环境Node环境)其实是有多个线程的,比如浏览器中还有GUI渲染线程http异步线程等,Node环境还有编译线程垃圾回收线程等。而单线程说的是执行JS代码的线程始终只有一个(除非Node环境中强制开启了子线程,process和child_process),这个线程也称为执行线程,也就是说,真正运行JS逻辑块的只有一个线程,哪怕是异步队列中的任务的逻辑块,也是在事件触发时(执行时机到达),从队列中取出,放入到执行线程中执行。因此,准确地描述是:绝大多数情况下,JS代码是通过唯一一个执行线程来执行的。

如果你想详细了解宏任务和微任务以及事件循环,可以阅读我的文章

从nextTick开始认识事件循环 - 掘金

此外,如果你想更深入了解Promise以及事件循环的工作流程,可以参考国外的这篇文章:

Promise可视化

queueMicroTask API

为了能够使得第三方库、polyfill等能够执行微任务,JS环境(浏览器和Node)暴露了全局的queueMicroTask接口,详细参考MDN。其可以将一个回调函数视为一个微任务加入到微任务队列中,就像:

1
2
3
4
queueMicroTask(()=>{
 const name="task"
 console.log(name)
})

这也为下文中我们完整地实现手写Promise提供了可能。

Promise A+规范

在手写前,我们必须了解一下

Promises/A+

,其为现如今各大主流浏览器实现Promise的一个规范和参考标准,因此我们要想自己实现Promise,也必须将其视为参考,并严格按照其规定实现。

首先,规定了一个promise实例(文中以大写的Promise表示统称、类和对象,以小写的promise表示Promise的实例)的状态一共为三类:pendingfulfilledrejected,要记忆的话,分别对应** 谈恋爱**、结婚了、**分手了**三种结果。但与结婚了还可以分手不同,三种状态一经改变不可逆转,也不可以发生fulfilled到rejected和rejected到fulfilled的转变,其描述如下图所示。

如果你已经使用过Promise,你会发现刚好对应着我们实例化时,智能拿到执行器中的resolvereject两个回调函数,其刚好代表着Promise状态的仅有的两条转化路径。

其次,Promise A+规定了Promise.prototype.then方法接收onFulfilledonRejected两个回调作为函数参数,最终返回一个新的Promise实例(记为p2)。而onFulfilledonRejected这两个回调函数各自接收一个参数,valuereason,分别代表兑现(fulfilled)值和拒绝(rejected)理由,如同结婚时的彩礼嫁妆和分手时给的理由,这两个值又分别由实例化时的两个回调函数resolvereject分别传入。

也因为Promise.prototype.then方法返回了p2,因此我们平时使用的Promise能够支持链式调用,但每次链式后都会返回一个新的**Promise**实例

手写前分析

在我们正式手写前,我们先通过一张思维导图来看看整个Promise的结构和所具备的方法:

可以看到除了Promise A+的状态以及Promise.prototype.then方法,原型方法还有:Promise.prototype.catchPromise.prototype.finally,静态方法有:Promise.resolvePromise.rejectPromise.anyPromise.allSetteldPromise.racePromise.all。其中核心就是构造函数和Promise.prototype.then方法的实现,尤其是Promise.prototype.then方法,其它原型方法基本要依赖其实现。

除了思维导图之外,还需注意一点,由于Promise支持链式使用,链式时返回的都是一个新的Promise实例new,这就涉及了newP的状态将由上一个promise的状态或者内部回调函数的逻辑决定(相当于还没结婚呢就开始买车买房,这些承诺之间相互影响)。且我们使用Promise时,经常使用catch在这个Promise链中捕获之前reject的reason或者逻辑块中的异常,例如:

1
2
3
4
5
6
7
8
9
10
11
12
Promise.reject(4).then((value)=>{
   console.log('p1 value',value)
   return value
}).then((value)=>{
   console.log('p2 value',value)
   return value
}).then((value)=>{
   console.log('p3 value',value)
   return value
}).catch((reason)=>{
   console.log(reason)
})

如果没有了catch,就会抛出异常:

也就是说,我们value或者reason会****在链式中向下传递,直到被使用或者捕获(统称为拦截)。

手写构造器部分

为了充分理解构造器,手写采用ES6的class语法,但必须注意的是,class语法也只是原型链prototype的语法糖,底层是一致的,javascript中的class并不是严格意义中的面向对象中的类。

将我们手写的Promise命名为TPromise,初始状态为pending

1
2
3
4
5
6
7
8
9
10
11
12
13
const PENDING="pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"

class TPromise{
   /**
    * @type {"pending"|"fulfiiled"|"rejected"}
    */
   status
   constructor(executor) {
       this.status=PENDING
  }
}

接着参考MDN上对于构造函数的描述,executor将会接收两个构造函数,分别用于更改Promise的状态。

  • resolve(vaue)
  • reject(reason)

**且我们知道,状态转变后就会执行之前添加的微任务(实际是将之前的任务添加到真实的微任务队列),那么,我们需要有一个队列存储我们传入的任务(回调函数),且状态从 **pendingfulfilledpendingrejected两条路径,于是也就会有两个队列来维护存储这些回调,于是代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class TPromise{
     /**
    * @private
    * @type {"pending"|"fulfilled"|"rejected"}
    */
  status
   /**
    * @private
    */
   value
   /**
    * @private
    */
   reason
   /**
    * @private
    * @type {Callback[]}
    */
  onFulfilledCallbacks
   /**
    * @private
    * @type {Callback[]}
    */
  onRejectedCallbacks
   constructor(executor) {
      const that = this;
       this.status = PENDING
       this.onFulfilledCallbacks=[];
       this.onRejectedCallbacks=[];

       if(typeof executor !== 'function'){
           throw TypeError('executor 必须是函数')
      }
       //分别构建resolve和reject函数传入
      function resolve(value){
           if(that.status === 'pending'){
               //只有pending时进入
               that.status='fulfilled';
               that.value=value;
               that.onFulfilledCallbacks.forEach(cb=>{
                   isFunc(cb) && cb(value)
              })
          }
      }
       function reject(reason){
           if(that.status === 'pending'){
               that.status='rejected'
               that.reason =reason;
               that.onRejectedCallbacks.forEach(cb=>{
                   isFunc(cb) && cb(reason)
              })
          }
      }

       //如果executor抛出异常,直接reject掉
       try{
           executor.call(that,resolve,reject)
      } catch (e){
           reject(e)
      }
  }
}

在之前的基础上,我们添加了onFulfilledCallbacksonRejectedCallbacks两个数组用于存储任务,并在定义的resolvereject函数中逐一调用,并使用传入的valuereason值作为回调的参数。尤其需要注意的是,resolvereject内部均判断了当前TPromise的状态是否还是pending,只有TPromise的状态还是pending才执行逻辑,满足Promise A+规范说的,状态只改变一次,不可逆转。且执行executor函数时,还要捕获内部的抛出的错误,如果抛出错误了,那么这个直接调用reject将状态设置为rejected,表示兑现失败。

手写Promise.prototype.then

then方法是整个Promise的灵魂所在,也就是它的内部创建了一个又一个的任务(回调函数),并立即将其加入到微任务队列,或是先添加到相对应的回调队列中等待resolvereject调用时添加到微任务队列中。

由于其返回一个新的TPromise实例(记为newP,支持链式调用的本质),有:

1
2
3
4
5
6
then(onFulfilled,onRejected){
       const that =this;
       return new TPromise((resolve,reject)=>{
           
      })
  }

接着的逻辑是:如果此时的状态已经发生转变,也就是在executor内部就调用了resolve或者reject,我们应该立即将任务送入微任务队列;如果状态还是pending,就应该把任务放入TPromise的回调队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

   then(onFulfilled,onRejected){
       const that =this;
       //不给对应的回调就把value和reason持续地向下传递
       onFulfilled = isFunc(onFulfilled)? onFulfilled :(value)=>value;
       onRejected = isFunc(onRejected)? onRejected:(reason)=>{
           throw reason
      };
       const promise= new TPromise(function(resolve,reject){
           //由上一个promise的状态决定新的promise是否立刻调用

           //方法封装
           //! 以下微任务也就是两个步骤,执行回调取值,得出结果就进一步判断结果的值的类型情况进一步兑现新创建的promise,
           //! 如果捕获到错误就直接reject
           function fulfilledCallback(value){
               queueMicrotask(()=>{
                   //!这里的逻辑块就是微任务
                   try{
                       const result = onFulfilled(value);
                       resolvePromise(promise,result,resolve,reject)
                  } catch (e){
                       reject(e)
                  }
              })
          }
           function rejectedCallback(reason){
               queueMicrotask(()=>{
                   //!这里的逻辑块就是微任务
                   try{
                       const result = onRejected(reason);
                       resolvePromise(promise,result,resolve,reject)
                  } catch (e){
                       reject(e)
                  }
              })
          }
           switch (that.status){
               //同步情况:调用queueMicroTask本身这个操作是同步的
               case 'fulfilled':
                   fulfilledCallback(that.value);
                   break;
               //同步情况
               case 'rejected':
                   rejectedCallback(that.reason);
                   break;
               default:
              {
                   //pending 状态,就是连微任务队列都没进,先暂存进入回调数组,
                   //待pending状态改变后再进入微任务队列中排队
                   //! 这里应用了发布订阅的设计模式
                   that.onFulfilledCallbacks.push(fulfilledCallback);
                   that.onRejectedCallbacks.push(rejectedCallback)
              }
          }
      });
       return promise
  }

先看swtich部分,其就是实现了上述所说的状态变就利用queueMicroTask加入微任务队列,状态不变入自身的回调队列。由于任务要么被直接加入微任务队列要么加入回调队列暂存,我先定义了fulfilledCallbackrejectedCallback函数,用来进一步封装并节省代码量。且可以通过代码看出,加入微任务队列这一操作还是同步操作,异步的微任务行为是最终从微任务队列取出执行的阶段,这一过程并不是我们控制的,我们真正做的还只是指定某个任务(回调函数)进入到微任务队列中!

而微任务中的逻辑块中

1
2
3
4
5
6
7
try{
   const result = onFulfilled(value);
   resolvePromise(promise,result,resolve,reject)
  }
catch (e){
          reject(e)
  }

其表示执行传入的onFulfilled,并获取返回值,并捕获其中抛出的错误,如果抛出错误,返回的新的TPromise实例就调用reject回调使其状态变为rejected,否则将继续判断result的值处理,而这部分比较复杂,又单独利用外部定义的resolvePromise函数处理。而3-4行是为了判断onFulfiiledonRejected回调是不是不传或者不是函数类型,如果是就把执行时传入的valuereason沿着Promise链向下传递,只有reasonthrow语句抛出,就是因为reject如果被自动调用,其都是在try...catch语句中的catch部分被调用,而你既然要被catch捕获,自然就要先抛出了,只有链式上的每一环都throw抛出,层层传递,才能被最终的最后的.catch((reason)=>)所捕获执行。

接着,解释下resolvePromise函数,先上完整版本的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function resolvePromise(promise,data,resolve,reject){
   if(data === promise){
      return reject(new TypeError('禁止循环引用'));
  }
   // 多次调用resolve或reject以第一次为主,忽略后边的
   let called = false
   if(((isObj(data)&& data!==null) || isFunc(data))){
       //这部分的写法是由Promise A+规范规定的
      try{
          const then = data.then
          if(isFunc(then)) {
              then.call(data, (value) => {
                  if (called) {
                      return
                  }
                  called = true
                  //递归执行,避免value是一个PromiseLike,Promise.resolve中的嵌套thenable在这里解决。
                  resolvePromise(promise, value, resolve, reject)
              }, (reason) => {
                  if (called) {
                      return
                  }
                  called = true
                  reject(reason)
                  }
              )
          } else{
              resolve(data)
          }
      } catch (e){
          if (called) {
              return
          }
          called = true
          reject(e)
      }
  } else{
       //data是null,undefined,普通引用值等
       resolve(data)
  }
}

首先,什么叫****循环引用,就是一个**Promise**状态的改变取决于自身的状态的改变,也就是先等我们结婚了再结婚,这显然是无稽之谈嘛,具体触发的可能情况在后续还会有说道,还请继续耐心看下去吧。

接着,让我们重新看看[Promise A+规范](https://promisesaplus.com/)。其谈到,对于onFulfilledonRejected的返回值(都记为result),如果其是一个Promise实例,就用其兑现后的状态设置newP的状态,如果其是一个对象且具有then方法,那么其是一个thenable对象,对result.then(onFulfilled,onRejected),执行并在其两个回调中,由于onFulfilled的参数value可能又是一个Promise实例或者thenable对象,递归调用resolvePromise函数,onRejected的参数reason则直接作为newPrejectedreason;其它情况下,newP的状态均为fulfilled,其流程如下图所示:

由此,Promise.prototype.then方法的逻辑已经完全实现,并严格遵循了Promise A+规范。

手写Promise.prototype.catch

MDN可知,Promise.prototype.catch只接收一个onRejected回调作为参数,其等价于this.then(null,onRejected)

因此,其内部实现为:

1
2
3
catch(onRejected){
return this.then(null,onRejected)
}

也就是说,其就是添加一个rejected时应该执行的微任务。

手写Promise.resolve

Promise.resolve() - JavaScript | MDN

可知,Promise.resolve是Promise上的一个静态方法,其将给定的值转为一个Promise,如果给定的值value就是一个Promise实例,直接返回;否则就直接返回一个新的Promise,并直接使用resolve将其兑现(可能是fulfilled也可能是rejected)。

于是有以下代码:

1
2
3
4
5
6
7
8
9
10
static resolve(value){
//!1.如果value是promise直接返回
if(value instanceof TPromise){
return value
}
return new TPromise((resolve)=>{
//thenable的情况实际上通过 resolvePromise完成了
resolve(value)
})
}

手写Promise.reject

MDN可知,静态方法Promise.reject返回一个已拒绝(rejected)的<font style="color:rgb(27, 27, 27);"> </font><font style="color:rgb(27, 27, 27);">Promise</font><font style="color:rgb(27, 27, 27);"> </font>对象,拒绝原因为给定的参数,无论给定的<font style="color:rgb(27, 27, 27);">reason</font>是什么,<font style="color:rgb(27, 27, 27);">reject</font>本身的行为就是拒绝,如同我和你分手的原因是因为我有了新欢,新欢将来结婚了还是分手还不知道,但我现在就要和你分手,这就是<font style="color:rgb(27, 27, 27);">reason</font>及时也是一个<font style="color:rgb(27, 27, 27);">Promise</font>对象也可以表示拒绝的理由,于是代码就很简单了。

1
2
3
4
5
6
static reject(reason){
//静态reject就是实例化后马上reject掉
return new TPromise((resolve,reject)=>{
reject(reason)
})
}

手写Promise.prototype.finally

MDN可知,Promise.prototype.finally是注册一个会在Promise兑现(无论是fulfilled还是rejected)时都会执行的函数,其不代表着Promise链的终结,依旧会返回一个等效Promise对象。如果处理程序抛出错误或返回被拒绝的 promise,那么 finally() 返回的 promise 将以该值被拒绝。否则,处理程序的返回值不会影响原始 promise 的状态。

这里的等效其实就是说,接到的value还要继续向下传递,接到的reason还得继续抛出,但如果finally强制指定了rejected状态的Promise,或者抛出错误,那么还是得rejected。看看以下代码:

1
2
3
4
5
6
7
8
Promise.resolve(5).finally(()=>{
console.log('finally fn')
}).then((value)=>{
console.log(value)
})
//output:
//finally fn
//5
1
2
3
4
5
6
7
8
Promise.reject(5).finally(()=>{
console.log('finally fn')
}).catch((reason)=>{
console.log(reason)
})
//output:
//finally fn
//5

其也利用Promise.prototype.then实现,代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
finally(onFinally){
//!假定 result = onFinally()
//! 使用 throw语句的原因在于我们只有在 try{} catch(e){ reject(e)} 的catch部分才会调用reject(),
//!也就是说,reason先被catch(捕获)才会被reject调用在promise中链式传递,finally不会处理
//!reason会让其继续传递,因此必须使用 throw 语句继续将其抛出,等待下游的try{} catch(e){} 将其再次捕获
//之所以用TPromise.resolve,是由于onFinally()的结果可能是Promise,必须等待其兑现此时的promise
return this.then(
//这个value为 pr.finally() 这个pr 的 fulfilled 状态下的value,它将不受result的影响传递下去
value=>TPromise.resolve(onFinally()).then(()=>value,
//这个reason为onFinally 显示指定一个 rejected的promise而产生,并传递下去
newReason=>{throw newReason}),
//这个reason 为 pr.finally() 这个pr 的 rejected状态下的 reason,只要 result不是一个rejected状态的promise,它将接着传递下去
(reason)=>TPromise.resolve(onFinally()).then(()=>{
throw reason
},(newReason)=>{
throw newReason
})
)
}

.finally(onFinally)这个Promise对象时,接收到其的valuereason,但onFinally()的返回值(记为fResult)未定,其可能是Promise对象也有可能是thenable对象,更有可能是其它类型,我们统一使用Promise.resolve将其处理为真的Promise对象fP,当fP敲定时(状态为fulfilled或者rejected

手写其它静态方法

以下几个静态方法都有一些共同的特性,给定一个可迭代对象(Array、Map、Set、String等任何具有[Symbol.iterator]属性的对象,如果你还不了解可迭代对象,请查阅MDN),将返回一个新的Promise对象。可迭代对象中的每一个元素可以是Promise对象也可以不是Promise对象,因此,内部就使用Promise.resolve将其处理为Promise对象,在此,约定把这些Promise对象称为p1、p2、p3、... 、pn,它们状态从pending转为fulfilled时的value值分别记为V1、V2、V3、... 、Vn,状态从pending转为rejected时的reason值分别为R1、R2、R3、... 、Rn。而每个方法放回的Promise对象,记为P,其状态都将由p1、p2、p3、... 、pn等共同决定。

在下述的示例图中,我会使用****蓝色表示pendingPromise对象,绿色表示fulfilledPromise对象,红色表示rejectedPromise对象。

Promise.all

MDN得知,Promise.all返回的Promise对象P,由p1、p2、p3、… 、pn 共同决定:只有所有的Promise对象被兑现为fulfilled,其P的状态才为fulfilled,且value值为所有Promise的value值的有序数组,value=[V1,V2,...,Vn]如图所示;

而只要p1、p2、p3、... 、pn 中任意一个Promise对象兑现为rejectedP的状态也为rejected,且reason值为第一个兑现为rejectedPromise的reason值(记为Rq),如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 static all(values){
//values不是一个可迭代对象就报错
if(!isIterator(values)){
throw new TypeError('values must be an iterable object.')
}
return new TPromise((resolve,reject)=>{
//返回结果,all,values
const results= [];
//fulfilled 计数器
let count =0;
//遍历顺序
let index =0;
//使用 for...of遍历可迭代对象
for(const value of values){
//避免闭包问题
let resultIndex = index;
index++;
const p = TPromise.resolve(value).then(value=>{
//!在此保证最终返回的promise,在fulfilled时,所有的兑现值均按参数传递时的顺序
results[resultIndex]= value;
//fulfilled中统计次数,一旦count和传入的promises长度相等,就说明所有的promise均fulfilled了。
count++
if(count === index){
resolve(results)
}
},(reason)=>{
reject(reason)
});
}
if(index===0){
//表示没有遍历,遍历对象为空
resolve(results)
}
})
}

//判断一个值是不是可迭代对象
function isIterator(val){
return typeof val[Symbol.iterator] === 'function'
}

其中,只解释如何使得在Promise兑现无序的情况下使得最终fulfilled状态时的value数组有序,下述的其它静态方法也同理。

就是实例化Promise时,就创建一个结果数组results,然后遍历传入的可迭代对象,并更新迭代的下标(由于我们使用for...of遍历一个可迭代对象,只能得到元素无法取得下标),很明显地,下标应该是有序的,当兑现为fulfilled时,按下标放入results数组中,而不是直接push;并使用一个计数变量count统计总的fulfilled的次数,当其和下标相等时,就是所有的Promise均为fulfilled,此时调用resolve回调兑现返回的P的状态,如果有任意Promise被兑现为rejected,就直接调用rejectP兑现为rejected,且由于P的状态已发生改变,就算后续其他Promise被兑现为rejected调用了reject回调,P的状态也不会再发生变化了。

Promise.any

MDN得知,Promise.any返回的Promise对象P,由p1、p2、p3、… 、pn 共同决定:只要任意一个Promise兑现为fulfilledP的状态为fulfilled,如下图所示:

当所有的Promie都被兑现为rejected时,P的状态为rejected,其reason=[R1,R2,...,Rn],如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static any(values){
if(!isIterator(values)){
throw new TypeError('values must be an iterable object.')
}
return new TPromise((resolve,reject)=>{
//结果,any ===> reasons
const results= []
//计数器,统计rejected 次数
let count =0;
//迭代时下标记录
let index=0;
for(const value of values){
//避免闭包问题
let resultIndex = index;
index++;
TPromise.resolve(value).then((value)=>{
resolve(value)
},reason=>{
results[resultIndex]=reason;
count++
if(count === index){
reject(results)
}
});
}
//如果下标不变,说明迭代对象为空
if(index===0){
reject(results)
}
})
}

//判断一个值是不是可迭代对象
function isIterator(val){
return typeof val[Symbol.iterator] === 'function'
}

Promise.race

MDN得知,Promise.race返回的P的状态随着第一个兑现的Promise对象决定。如果第一个兑现为fulfilledP也兑现为fulfilled,且value值和其等同,如下图所示:

当 第一个兑现的为rejectedP也兑现为rejected,且reason值和其等同,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 static race(values){
if(!isIterator(values)){
throw new TypeError('values must be an iterable object.')
}
return new TPromise((resolve,reject)=>{
//遍历下标
let index =0;
for(const value of values){
//避免闭包问题
let resultIndex = index;
index++;
TPromise.resolve(value).then((value)=>{
resolve(value)
},(reason)=>{
reject(reason)
});
}
})
}

//判断一个值是不是可迭代对象
function isIterator(val){
return typeof val[Symbol.iterator] === 'function'
}

Promise.allSettled

从MDN得知,Promise.allSettled返回一个状态为fulfilledPromise对象,其为p1、p2、p3、... 、pn 全部兑现后结果的有序数组,并将同时记录其兑现后的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static allSettled(values){
if(!isIterator(values)){
throw new TypeError('values must be an iterable object.')
}
return new TPromise((resolve)=>{
const results = [];
//计数器,兑现个数统计
let count=0;
//迭代下标
let index=0;
for(const value of values){
//避免闭包问题
let resultIndex = index;
index++;
TPromise.resolve(value).then((value)=>{
//保证有序
results[resultIndex]={
status:'fulfilled',
value:value
};
count++;
if(count===index){
resolve(results)
}
},(reason)=>{
//保证有序
results[resultIndex]={
status:'rejected',
reason:reason
};
count++;
if(count===index){
resolve(results)
}
});
}
//可迭代对象为空
if(index===0){
resolve(results)
}
})
}

//判断一个值是不是可迭代对象
function isIterator(val){
return typeof val[Symbol.iterator] === 'function'
}

测试我们手写的promise

手写完我们自己的TPromise后,我们还需要进一步确认我们实现的TPromise是否全部符合Promise A+规范,为此,Promise A+官方提供了一个测试库promises-aplus-tests。新建一个adapter.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
module.exports={
resolved:TPromise.resolve,
rejected:TPromise.reject,
deferred(){
const result = {};
result.promise = new TPromise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
}

再新建一个test.js文件,使用这个测试库测试,其共拥有****872个测试用例,通过了就真正完成了我们的手写Promise的全部过程。

1
2
3
4
5
6
7
8
9
10
11
const promisesAplusTests = require('promises-aplus-tests');
const adapter = require('./adapter');

promisesAplusTests(adapter, function (err) {
if (err) {
console.error('Promises/A+ 测试失败:');
console.error(err);
} else {
console.log('Promises/A+ 测试通过');
}
});

小结

手写部分逐个分析了Promise的构造函数、三个原型方法和六个静态方法。其中最重要的就是Promise.prrototype.thenPromise.resolve两个方法,因为其除了自身逻辑复杂,还被其他方法使用到,如果要在面试时利于不败之地,必须每行代码都要吃透。同时,通过手写,我们也逐步了解到,Promise真正异步的逻辑部分是使用thencatchfinally三个原型方法的回调函数部分,且**Promise**本身不执行微任务,而是把微任务放入到**javascript**执行线程中的微任务队列中。且Promise状态的流转只能发生一次,状态一经改变就意味着Promise已经兑现。

记忆时,可以参考刚开始的思维导图,三个原型方法都支持链式调用;六个静态方法都会返回Promise,按单词本身理解是记忆的最好方法。

方法 记忆 是否一定返回新的Promise
Promise.resolve resolve为解决,解决什么,当然是解决value为Promise
Promise.reject reject为拒绝,当然是无条件拒绝
Promise.all all,所有,所有的都“成功”
Promise.any any,任意,任意一个”成功“
Promise.race race,竞赛,状态是否改变的时间竞赛
Promise.allSettled allSettled,所有的都解决,所有的Promise都兑现了

async/await 是Promise的语法糖

async/await允许我们以一种更为简洁的方式实现promise的异步编程,省去我们链式操作的烦恼,其本质也是对于Promise的封装,是语法糖。经常有人说,async/await使得Promise同步化,但其本质还是Promise,你的代码逻辑还是会被送入微任务队列,怎么就同步了呢?还有的说法是await会阻塞代码,其实并不是,javascript可是号称非阻塞线程的,如果await真的阻塞一个几天以后才会执行的代码那还得了!那await其实只是把其后的逻辑块处理为一个返回promise的的异步函数,这也很容易让我们联想到Promise.resolve静态方法,其包裹的值将被处理为一个Promise对象,

如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function asyncFn(){
console.log(1);
const res = await aa();
console.log(res);
console.log(2)
}

function aa(){
console.log(5)
return 3
}
asyncFn()
console.log(4)

//output: 1 5 4 3 2

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function asyncFn(){
console.log(1)
return Promise.resolve(aa()).then(()=>{
console.log(res)
console.log(2)
})
}

function aa(){
console.log(5)
return 3
}
asyncFn()
console.log(4)

JavaScript中,如果函数不指定返回值,默认会返回undefined,于是上述的代码再次等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function asyncFn(){
console.log(1)
return Promise.resolve(aa()).then(()=>{
console.log(res)
console.log(2)
return undefined
})
}

function aa(){
console.log(5)
return 3
}
asyncFn()
console.log(4)

且我们知道,Promise.prototype.then方法接收的onFulfilled回调的返回值又将决定新的Promise的状态,如果它不是thenable对象也不是Promise,它直接作为新Promise的value值。于是,下述的代码将输出undefined

1
2
3
asyncFn().then((value)=>{
console.log(value)
})

这个undefined的值并非async/await指定,而是函数默认的return undefined这一行为所导致,async/await只是做了一层包裹,于是乎,我们得到了async/await语法糖的实质:

1
2
3
4
5
6
7
8
9
10
async function(){
await xxx
}

//等价于
function (){
return Promise.resolve(xxx).then(()=>{

})
}

由于Promise.resolve可以传入一个promise实例或者thenable对象,我们来看看函数aa如果也是返回一个promise的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function asyncFn(){
console.log(1);
const res = await aa();
console.log(res);
console.log(2)
}

function aa(){
console.log(5)
return Promise.reject(3)
}
asyncFn()
console.log(4)

//output: 1 5 4 3

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function asyncFn(){
console.log(1);
return Promise.resolve(aa())
.then((res)=>{
console.log(res);
console.log(2);
return undefined
},reason => {
console.log(reason)
})
}

function aa(){
console.log(5)
return Promise.reject(3)
}
asyncFn()
console.log(4)

可以明确看出,由于函数aa返回的是rejected状态的promise,最终输出不会有2,而是输出 1、5、4、3。其中,由于Promise.resolve如果传入的是一个promise实例将直接返回,所以此刻在函数asyncFn中并不会新建一个promise实例,而是直接使用函数**aa**返回的**promise**实例

再次强调:*构造函数以及**resolve****reject**调用的过程均是同步行为,只有**then***、**catch****finally**三个原型方法传入的回调才会异步执行,且这三个原型方法调用的本身都是同步行为。

接下来我们再看,函数内有多个await的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function asyncFn(){
console.log(1);
const res = await aa();
console.log(res);
console.log(2)
const res2=await bb();
console.log(6)
console.log(res2)
}

function aa(){
console.log(5)
return 3
}

function bb(){
console.log(7)
return Promise.resolve(8)
}
asyncFn()
console.log(4)

//output 1 5 4 3 2 7 6 8

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function asyncFn(){
console.log(1);
return Promise.resolve(aa()).then(async (res)=>{
console.log(res);
console.log(2)
const res2=await bb();
console.log(6)
console.log(res2)
})
}

function aa(){
console.log(5)
return 3
}

function bb(){
console.log(7)
return Promise.resolve(8)
}
asyncFn()
console.log(4)

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function asyncFn(){
console.log(1);
return Promise.resolve(aa()).then(async (res)=>{
console.log(res);
console.log(2)
return Promise.resolve(bb()).then(
res2=>{
console.log(6)
console.log(res2)
return undefined;
}
)
})
}

function aa(){
console.log(5)
return 3
}

function bb(){
console.log(7)
return Promise.resolve(8)
}
asyncFn()
console.log(4)

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function asyncFn(){
console.log(1);
console.log(5);
const aaReturn = 3;
return new Promise((resolve)=>{
resolve(aaReturn)
}).then((res)=>{
console.log(res);
console.log(2);
console.log(7);
// return Promise.resolve(8).then((res2)=>{
// console.log(6);
// console.log(res2)
// })
return new Promise((resolve)=>{
resolve(8)
}).then(res2=>{
console.log(6);
console.log(res2)
})
})

}

function aa(){
console.log(5)
return 3
}

function bb(){
console.log(7)
return Promise.resolve(8)
}
asyncFn()
console.log(4)

由此,如果存在多个await,先使用Promise.resolve替换第一个await语句,然后将剩下的语句塞入其.then方法的onFulfilled回调函数中,并且把async关键字挪到回调之前,即onFulfilled回调函数此时也是一个async/await函数,也就说明这个onFulfilled回调也将返回一个promise实例。重复此操作不停内嵌,直至所有的await语句被替换。最终,函数asyncFn返回的promise由最后一句await其后的返回所决定,由下述代码所输出的那样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function asyncFn(){
console.log(1);
console.log(5);
const aaReturn = 3;
return new Promise((resolve)=>{
resolve(aaReturn)
}).then((res)=>{
console.log(res);
console.log(2);
console.log(7);
// return Promise.resolve(8).then((res2)=>{
// console.log(6);
// console.log(res2)
// })
return new Promise((resolve)=>{
resolve(8)
}).then(res2=>{
console.log(6);
console.log(res2)
return 9 //新增
})
})

}

asyncFn().then(value=>{
console.log(value) //输出9
});
console.log(4);

//output: 1 5 4 3 2 7 6 8 9

且经过转换后,我们也看到了如果没有async/await语法糖,多个**promise**的嵌套将会引发我们经常听说的回调地狱,而有了async/await就可以解决这个问题,增强了我们代码的可读性(但确实不利于直观地明白输出顺序了,面试害死人……)

经典面试题

以下举例两道经典的面试题,请你先将其async/await等价替换后,给出输出的结果,最终答案以及解析将发在评论区,欢迎留言讨论哦。

面试题01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function async1() {
console.log("A")
await async2()
console.log("B")
}

async function async2() {
console.log('C');
}

console.log('D')

setTimeout(function () {
console.log('E')
}, 0)

async1();

new Promise(function (resolve) {
console.log('F')
resolve()
}).then(function () {
console.log('G')
})

console.log('H')

面试题02

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async function asy1(){
console.log(1);
await asy2();
console.log(2);
}

const asy2 = async ()=>{
await setTimeout(()=>{
Promise.resolve().then(()=>{
console.log(3)
});
console.log(4);
},0)
};

const asy3 = async ()=>{
Promise.resolve().then(()=>{
console.log(6);
})
}

asy1();
console.log(7);
asy3();

补充1:构造函数中resolve的进一步解释说明

将以下的代码通过 Chrome、Edge、FireFox浏览器以及Node环境下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new Promise(resolve=>{
const resolvedPromise=Promise.resolve()
resolve(resolvedPromise)
}).then(()=>{
console.log('resolved promise')
})

Promise.resolve()
.then(()=>{
console.log('promise1')
})
.then(()=>{
console.log('promise2')
})
.then(()=>{
console.log('promise3')
})

1
2
3
4
promise1
promise2
resolved promise
promise3

按照我们的构想以及测试我们手写的TPromise,输出的结果似乎为:

1
2
3
4
resolved promise
promise1
promise2
promise3

总结

本文按照Promise A+规范利用queueMicroTaskAPI手写了Promise,并解释了微任务产生以及执行的具体时机。

另外,介绍了promises-plus-tests库用于测试我们的手写Promise是否完全符合Promise A+规范

最后,解释了async/await如何等价转换为Promise,并留下两道经典面试题作为思考。

所有的手写代码可以在我哥github仓库查看。