JavaScript Promise 详解

Promise 规范有很多,如 Promises/A、Promises/B、Promises/D 以及 Promises/A+。最终 ES6 中采用了 Promises/A+规范。

Promises/A+规范,有现成的单元测试套件,很容易搭建开发环境,以及验证代码是否符合规范要求。我们可以按照 Promises/A+ 规范,来从头实现一个 MyPromise。

1、Promise 的状态

一个 Promise 的状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)、拒绝态(Rejected)

等待态(Pending)

处于等待态时,promise 满足以下条件:

可以迁移至执行态或拒绝态

执行态(Fulfilled)

处于执行态时,promise 满足以下条件:

  • 不能迁移至其他任何状态

  • 必须拥有一个不可变的终值

拒绝态(Rejected)

处于拒绝态时, promise 满足以下条件:

  • 不能迁移至其他任何状态

  • 必须拥有一个不可变的拒因

术语解释

promise:是一个包含 then 方法的对象或函数,该方法符合规范(即 Promises/A+)制定的行为。

thenable:是一个包含 then 方法的对象或者函数。

终值(eventual value):所谓的终值,指的是 promise 被解决时传递给解决回调的值,因为 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之为终值,有时候也直接简称为值(value),值可以是任何 JavaScript 合法值(包括 undefined、thenable 和 promise)

拒因(reason):也就是拒绝原因,指的是 promise 被拒绝时传递给拒绝回调的值。

解决(fulfill):指一个 promise 成功时(调用 resolve )进行的一系列操作,如状态的改变,回调的执行。虽然规范中用 fulfill 表示解决,但现在的 promise 实现中多以 resolve 指代之。

拒绝(reject): 指一个 promise 失败时(调用 reject 或抛出异常)进行的一系列操作。

exception(异常):就是 throw 语句抛出的值。

代码更新

2、Promise 构造函数

Promise 是一个构造函数,传参是一个函数,传入的函数接受两个参数,一个是解决回调 resolve,一个拒绝回调 reject。

代码更新

resolve 方法将 promise 的状态改为 Fulfilled ,并传入一个不可变的终值,reject 方法将 promise 的状态改为 Rejected,并传入一个不可变的拒因。promise 的状态一旦改为 Fulfilled 或者 Rejected 就不能再迁移至其他状态。

代码更新

调用构造函数的参数 fn,将 resolve 和 reject 作为参数传入 fn,记得加上 try,如果捕获到错误就 reject。

代码更新

3、Then 方法

一个 promise 必须提供一个 then 实例方法以访问其终值和拒因。

promise 的 then 方法接受两个参数:

promise.then(onFulfilled, onRejected)

onFulfilled 和 onRejected 都是可选参数。

代码更新

onFulfilled

如果 onFulfilled 不是函数,其必须被忽略。所谓的“忽略”并不是什么都不干,对于 onFulfilled 来说,忽略就是将接收到的 value 直接返回。

如果 onFulfilled 是函数:

  • 当 promise 执行结束后其必须被调用,其第一个参数为 promise 的终值

  • 当 promise 执行结束前其不可被调用

  • 其调用次数不可超过一次

onRejected

如果 onRejected 不是函数,其必须被忽略。所谓的“忽略”并不是什么都不干,对于 onRejected 来说就是返回 reason,因为 onRejected 是一个拒绝分支,触发之后的拒绝状态,应该抛出一个异常,所以返回的 reason 应该 throw 一个 Error

如果 onRejected 是函数:

  • 当 promise 被拒绝后其必须被调用,其第一个参数为 promise 的拒因

  • 在 promise 被拒绝之前其不可被调用

  • 其调用次数不可超过一次

调用时机

onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用。实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在then方法被调用的那一轮事件循环之后的新执行栈中执行。

调用要求

onFulfilled 和 onRejected 必须被作为函数调用(即没有 this 值)

多次调用

then 方法可以被同一个 promise 调用多次

  • 当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调

  • 当 promise 被拒绝时,所有的 onRejected 需按照其注册顺序依次回调

代码更新

返回

then 方法必须返回一个新的 promise 对象

promise2 = promise1.then(onFulfilled, onRejected)

  • 如果 onFulfilled 或者 onRejected 返回一个值 x,则运行下面的Promise 解决过程[[Resolve]](promise2, x)

  • 如果 onFulfilled 或者 onRejected 抛出一个异常 e,则 promise2 必须被拒绝,并返回拒因 e

  • 如果 onFulfilled 不是函数且 promise1 成功执行,promise2 必须成功执行并返回相同的值

  • 如果 onRejected 不是函数且 promise1 被拒绝,promise2 必须被拒绝并返回相同的拒因。

Promise 解决过程

onFulfilled 或者 onRejected 返回一个值 x,则运行 Promise 解决过程。

Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值 x,我们表示为[[Resolve]](promise2, x),如果 x 有 then 方法,且看上去像一个 Promise(thenable),解决程序及尝试使 promise2 接受 x 的状态;否则其用 x 的值来执行(resolve) promise2。

运行[[Resolve]](promise2, x)需遵循以下步骤:

x 与 promise2 相等

如果 promise2 和 x 指向同一对象,以 TypeError 为拒因拒绝 promise2。

x 为 Promise

如果 x 为 Promise,则使 promise2 接受 x 的状态:

  • 如果 x 处于等待态,promise2 需保持为等待态直至 x 被执行或拒绝

  • 如果 x 处于执行态,用相同的值执行 promise2

  • 如果 x 处于拒绝态,用相同的拒因拒绝 promise2

x 为对象或函数

如果 x 为对象或者函数:

  • x.then 赋值给 then

  • 如果取 x.then 的值时抛出错误 e,则以 e 为拒因拒绝 promise2

  • 如果 then 是函数,将 x 作为函数的作用域 this 调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise,第二个参数叫做 rejectPromise:

    • 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise2, y)

    • 如果 rejectPromise 以拒因 r 为参数被调用,则以拒因 r 拒绝 promise2

    • 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用

    • 如果调用 then 方法抛出了异常 e:

      • 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之

      • 否则以 e 为拒因拒绝 promise2

  • 如果 then 不是函数,以 x 为参数执行 promise2

如果 x 不为对象或者函数,以 x 为参数执行 promise2

最终代码汇总

其他 Promise 的方法

ES6 的 Promise 还有很多 API,这些 API 都可以用我们按照 promise/A+规范实现的代码,再进行封装得到。

Promise.resolve

返回一个以给定值解析后的 Promise 实例,换句话说就是将传入的值转换为 Promise 对象。如果传参是 Promise,直接返回,如果是 thenable 对象,返回的 Promise 实例会采用 thenable 的运行后的状态,如果不是前面两种类型,那就返回一个新的 Promise 实例,状态为 Fulfilled。

Promise.reject

返回一个新的 Promise 实例,该实例的状态为 Rejected。Promise.reject 方法的参数 reason,会被传递给实例的回调函数。

Promise.all

该方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

该方法接收的参数是 promise 的 iterate 类型(例如:Array、Map、Set),一般是一个数组,并且只返回一个 Promise 实例,这个实例的 resolve 回调结果是一个数组。必须注意:返回的这个数组每一项的顺序将会按照传入时参数的 promise 顺序排列,而不是由调用 promise 的完成顺序决定

传入参数 iterate 类型中每一项都是 promise 实例,如果不是,就会先调用 Promise.resolve 方法转为 Promise 实例,再进一步处理。当 iterate 类型的每一项都 resolve,返回的 promise 才 resolve,有任何一个 reject ,返回的 promise 就 reject,并且 reject 的是第一个抛出的错误信息。

Promise.race

该方法同样也是将多个 Promise 实例包装成一个新的 Promise 实例。

Promise.race(iterate) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

Promise.allSettled

ES2020 引入,该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数示例都返回结果,不管是 fulfilled 还是 rejected ,新的 Promise 才会结束,一旦结束,状态总是 fulfilled, 不会变成 rejected。状态变成 fulfilled 后,解决回调函数接收到的参数是一个数组,每一项表示对应的 promise 的结果,并且每一项的格式为 {status: "fulfilled", value: xxx} 或 {status: "rejected" reason: xxx}

Promise.any

ES2021 引入,该方法接受一个 promise 可迭代对象,只要其中一个 promise 成功,就返回那个已经成功的 promise,如果所有的 promise 都失败了,就返回一个失败的 promise,拒因为一个把单个错误合在一起的 AggregateError。本质上,此方法跟 Promise.all 相反。

Promise.prototype.catch

Promise.prototype.catch 方法是 .then(null, onRejected).then(undefined, onRejected) 的别名,用于指定发生错误时的回调函数。

如果 onRejected 抛出一个错误或返回一个本身失败的 Promise,通过 catch() 返回的 Promise 会被 reject;否则,返回的 Promise 会被 resolve。

Promise.prototype.finally

用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入的。

一些自己实现的其他的方法

测试

我们可以使用 Promise/A+官方的测试工具 promises-aplus-tests 来对自己实现的 MyPromise 进行测试,要使用这个工具我们必须实现一个静态方法 deferred :

deferred:返回一个包含 {promise, resolve, reject} 的对象 promise 是一个处于 pending 状态的 promise resolve(value) 用 value 解决上面那个 promise reject(reason) 用 reason 拒绝上面那个 promise

新建一个 adater.js 文件:

然后再 npm install promises-aplus-tests , 在配置好 package.json :

npm run test 就可以跑测试了

注意

ES6 Promise 中 resolve 传值

ES6 的 Promise,在调用 resolve,传入一个 promise 实例时,即使在 onFulfilled 中不返回这个实例,后续的链式操作,也会以这个传入的实例状态为准,而我们自己实现的 MyPromise 必须在 onFulfilled 中返回传入的实例,走到 resolvePromise 环节,后续的链式操作才会以传入的实例状态为准。

推荐文章

Last updated