详解Promise

谈到Promise,那我们不得不提到这篇文章:Promises/A+

Promise A+

An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.

有兴趣可以看看原文,虽然原文是全英文的,但是可以借助翻译软件阅读。本篇文章也是参考Promise A+规范和ES的Promise实现进行分析,再结合我个人理解进行编写,如有错误,欢迎指出,我将不甚感激!

Promise状态

Promise共有三种状态:pending、fulfilled、rejected。

  1. 未完成时(unsettled),处在pending状态;

  2. 完成时(settled),只能是fulfilled、rejected状态的其中一个,并且选择其中一个状态,那么该promise的状态就不可再变化了。fulfilled状态会携带一个不可变的值,rejected状态会携带一个不可变的原因。

then方法

Promise必须提供一个then方法,且该方法接受两个参数:promise.then(onFulfilled, onRejected)

  1. onFulfilled和onRejected都是可选参数,并且如果不是函数,则会被忽略;

  2. onFulfilled如果是函数,则promise状态变成fulfilled时,该函数会被执行,并且执行时传入的第一个参数,是promise的值;

  3. onRejected如果是函数,则promise状态变成rejected时,该函数会被执行,并且执行时传入的第一个参数,是promise的原因;

  4. 执行onFulfilled、onRejected这两个回调函数时,其实是将函数推到微队列中执行的。

本质上promise.then方法会产生一个新的promise,假设现在有这样一个调用场景:

javascript
const p1 = new Promise(...) const p2 = p1.then(onFulfilled, onRejected)
  1. 假设onFulfilled和onRejected都是函数
    1. 假如p1一直处在pending状态,则onFulfilled和onRejected在p1改变状态之前都不会被执行,则p2也一直是pending;
    2. 假如p1变成了fulfilled,则执行onFulfilled;如果onFulfilled执行完成,则其函数返回值作为不可变得值一起把p2给fulfilled掉;如果执行onFulfilled期间抛出错误,则错误会被作为不可变得原因给p2给rejected掉;
    3. 如果p2变成了rejected,则执行onRejected;如果onRejected执行完成,则其函数返回值作为不可变得值一起把p2给fulfilled掉;如果执行onFulfilled期间抛出错误,则错误会被作为不可变得原因给p2给rejected掉;
  2. 假设onFulfilled和onRejected不知道是啥
    1. 假如p1一直在pending状态,函数执行情况和p2状态都与上述情况一致;
    2. 假如p1变成了fulfilled,如果此刻onFulfilled不是函数,则p2的状态会变得和p1一致(fulfilled),且值也与p1一致;
    3. 假如p2变成了rejected,如果此刻onRejected不是函数,则p2的状态会变得和p1一致(rejected),且原因也与p1一致;

catch方法

结合上述then方法的规则,此刻让我们再联系上Promise实现的.catch方法。看起来是多了一个方法,但其实本质上.catch也是生成一个promise,作用和情况都有点类似.then(null, reject => ...)。两者在实用上的区别就在于,p1.then(onFulfilled, onRejected) 用法中,onRejected函数,只能处理p1的rejected;而在栗子p1.then(onFulfilled).catch(onRejected) 中,onRejected既可以处理p1的rejected,也可以处理p1.then(onFulfilled)生成的Promise的rejected。


至此,事情就开始变得有趣了起来,知道了.then和.catch方法会生成一个新的Promise,并且函数的返回值和抛出的错误会作为生成的Promise的值和错误,进而允许触发下一个依赖其状态的Promise执行特定的方法,再结合上述的特性,我们就可以完成链式调用的效果了!

语法糖await

在ES6中,Promise用于实现异步操作,解决了回调地狱的问题,但依旧没有解决回调(还是用的.then方法进行回调函数注册)。在ES7中,出现了async和await关键字,解决了回调,为异步函数提供了像同步函数的调用的方式和代码风格。

await遇到:

  1. 普通对象,则得到的是原始的普通对象。例如

    javascript
    const o = { a: 1 }; const v = await o;

    其中await o可以等同为await Promise.resolve(o),最后得到的结果(变量v)与o完全一致(即v === o为true)

  2. Thenable对象或Promise,该过程会触发吸收机制,如果是Thenable对象则会调用其.then方法,如果是Promise对象则等待其状态确定,试图获取最终的“值”

另外,await本质是等同于.then的语法糖,因此下面的代码,输出的结果是2 1,因为代码执行完const o = 1await o;,遇到了await关键字,等同于剩余的const v = 及后续代码是放到微队列中的,而等待执行期间,由于console.log(2)属于接下来需要执行的同步代码,则2先被打印,后续微队列中赋值变量v和打印v的代码才被执行,因此1后被打印。

javascript
(async () => { const o = 1; const v = await o; console.log(v); })() console.log(2)

Promise静态方法

  1. Promise.resolve(data)

    1. 作用上等同与new Promise((resolve) => resolve(data)),但又有一些区别;

    2. 如果data是一个promise,则直接返回这个promise,可用这段代码检测:

      javascript
      const p1 = new Promise(() => {}); const p2 = Promise.resolve(p1); console.log(p1 === p2); // true
    3. 如果data是一个有.then方法的非Promise对象(称之为Thenable),则调用其.then方法,并向其传递一个新Promise的resolve和reject方法,并返回这个新的Promise;

    4. 如果data不是上述情况中的类型,则返回的Promise就真的是这样的效果了:new Promise((resolve) => resolve(data))

  2. Promise.reject(reason)

    1. 作用上等同与new Promise((null, reject) => reject(reason)),并且不会像Promise.resolve那样对promise和带.then方法的对象进行特殊处理。

这几个方法都是传入一个数组(传入的值是promise或者不是都行),返回的都是Promise对象

  1. Promise.all(...):所有任务均resolve,才会fulfilled,值是每个任务fulfilled的值(数组);只要一个失败,则失败,原因是失败的任务的原因;
  2. Promise.any(...):只要有一个任务resolve,就fulfilled,值是成功的任务的值;只有所有任务都失败,才失败,失败的原因是一个封装的对象,对象errors属性记录了每个任务reject的原因(也是数组);
  3. Promise.allSettled(...):所有任务被resolve或reject(即非pending或unsettled状态,应该叫settled),就成功,值是一个由{status: 'fulfilled', value: ...}{status: 'rejected', reason: ...}对象组成的数组;没有失败的情况;
  4. Promise.race(...):选出最快被resolve或reject的任务,状态和值或原因 与最快settled的任务相同,但并不是同一个promise。

补充

  • 关于Thenable对象。 其中Promise.resolve、promise.then的onFulfilled回调返回值、new Promise((resolve)=>...)的resolve方法 都会对Thenable对象进行特殊处理。(甚至await关键字后也可以跟一个非Promise的Thenable对象,就是因为其本质上就是.then的语法糖)
  • 关于ES Promise的吸收机制。 Promise会帮我们把promise或thenable对象自动进行解析,直到得到最终的值,因此吸收操作本质上是resolve操作的传递性,也就是说,除了reject,基本上其他的大部分方法都会涉及到promise吸收机制。(.then中throw、Promise.allSettled也不会进行吸收操作)