Promise 介绍
文章目录
基础介绍
Promise 对象用于一个异步操作的最终完成(或失败)及其结果值的表示。(简单点说就是处理异步请求。我们经常会做些承诺,如果我赢了你就嫁给我,如果输了我就嫁给你之类的诺言。这就是promise的中文含义:诺言,一个成功,一个失败。)
Promise表示一个异步操作的结果,与之进行交互的方式主要是then
方法,该方法注册了两个回调函数,用于接收promise
的终值或本promise
不能执行的原因。
如下代码:
1 | var p = new Promise(function(resolve, reject){ // executor |
new Promise
实例化一个Promise,其第一个构造参数executor
是一个带有resolve
和reject
两个参数的函数,executor
函数在Promise构造函数返回新建对象前被调用,被传递resolve
和reject
函数。resolve
和reject
函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor
内部通常会执行一些异步操作,一旦完成,可以调用resolve
函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。
- 接受(fulfill):指一个
promise
成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用fulfill
来表示接受,但在后世的promise
实现多以resolve
来指代之。 - 拒绝(reject):指一个
promise
失败时进行的一系列操作。 - 终值(eventual value):所谓终值,指的是
promise
被接受时传递给解决回调的值,由于promise
有一次性的特征,因此当这个值被传递时,标志着promise
等待态的结束,故称之终值,有时也直接简称为值(value)。 - 据因(reason):也就是拒绝原因,指在
promise
被拒绝时传递给拒绝回调的值。
三个状态
三个状态:未被决议、完成、拒绝,决议完状态不再改变
- pending: 等待态,移至执行态或拒绝态
- fulfilled: 接受态,不能迁移至其他任何状态,必须拥有一个不可变的终值
- rejected: 拒绝态,不能迁移至其他任何状态,必须拥有一个不可变的据因
这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(译者注:指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。
如下代码:
1 | var p = new Promise(function(resolve, reject) { |
变量p
为一个已经决议的promise
,它的决议状态(PromiseStatus
)一直都是resolved,值一直未42
。
Then
一个promise
必须提供一个then
方法以访问其当前值、终值和据因。
1 | promise.then(onFulfilled, onRejected) |
参数
- onFulfilled:当Promise变成接受状态(fulfilled)时,该参数被调用。该函数有一个参数,即接受的值。
- onRejected:当Promise变成拒绝状态(rejected)时,该参数被调用。该函数有一个参数,即拒绝的原因。
返回值
then方法返回一个Promise,而它的行为与then中的回调函数的返回值有关:
- 如果
then
中的回调函数返回一个值,那么then
返回的Promise将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。- 如果
then
中的回调函数抛出一个错误,那么then
返回的Promise将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。- 如果
then
中的回调函数返回一个已经是接受状态的Promise,那么then
返回的Promise也会成为接受状态,并且将那个Promise的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。- 如果
then
中的回调函数返回一个已经是拒绝状态的Promise,那么then
返回的Promise也会成为拒绝状态,并且将那个Promise的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。- 如果
then
中的回调函数返回一个未定状态(pending)的Promise,那么then
返回Promise的状态也是未定的,并且它的终态与那个Promise的终态相同;同时,它变为终态时调用的回调函数参数与那个Promise变为终态时的回调函数的参数是相同的。
如下代码:
1 | var p1 = new Promise(function(resolve, reject) { // executor |
- 如果
onFulfilled
不是函数且p1
成功执行,p2
必须成功执行并返回相同的值 - 如果
onRejected
不是函数且p1
拒绝执行,p2
必须拒绝执行并返回相同的据因
如下p1
成功执行,p2
的onFulfilled
不是函数,p2
返回相同的值:
1 | var p1 = Promise.resolve('终值'); // 等价于 new Promise(function(resovle) { resovle('终值') }); |
catch
1 | var p1 = Promise.resolve('终值'); |
resolve
构造参数executor
的resolve
接受一个参数(和then中的回调函数返回值处理流程一样):
- 传递的不为
promise
或thenable
,则返回一个决议成功的promise,结果为传入的参数 - 传递的为
promise
或thenable
,则结果和此对象相同
1 | var p = new Promise(function(resovle) { resovle('终值') }); |
Promise.resolve
Promise.resolve
提供一个简便的方法来得到接受的promise
1 | Promise.resolve('终值'); |
不同的是,如果Promise.resolve
的入参为promise
或thenable
,则直接返回入参。
1 | var a = new Promise(function(resovle) { resovle('终值') }); |
Promise.reject
类似Promise.reject
是一个简便的方法来得到拒绝的promise
1 | Promise.reject('终值'); |
构造参数executor
的reject
和Promise.reject
接受一个参数,无论改参数是何种状态的promise
还是普通值,都将其作为据因
其它细节
执行时间
【翻译】Promises/A+规范中对此解释的很清楚:
注1 这里的平台代码指的是引擎、环境以及
promise
的实施代码。实践中要确保onFulfilled
和onRejected
方法异步执行,且应该在then
方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。由于promise
的实施代码本身就是平台代码(译者注: 即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。译者注: 这里提及了
macrotask
和microtask
两个概念,这表示异步任务的两种分类。在挂起任务时,JS引擎会将所有任务按照类别分到这两个队列中,首先在macrotask
的队列(这个队列也被叫做task queue
)中取出第一个任务,执行完毕后取出microtask
队列中的所有任务顺序执行;之后再取macrotask
任务,周而复始,直至两个队列的任务都取完。两个类别的具体分类如下:
- macro-task: script(整体代码),
setTimeout
,setInterval
,setImmediate
, I/O, UI rendering- micro-task:
process.nextTick
,Promises
(这里指浏览器实现的原生 Promise),Object.observe
,MutationObserver
详见stackoverflow 解答 或 这篇博客
补充一句:在处理微任务microtask
时,他们可以排队更多的微任务,这些微任务都将被逐个运行,直到微任务队列耗尽。然后在再取macrotask
任务。
考虑以下代码:
1 | var p1 = new Promise(function(resovle) { // executor |
序号表示控制台打印的顺序,可以看出executor
是同步执行的,在console.log('同步执行')
执行后,
第一个onFulfilled
、第三个onFulfilled
、第二个onFulfilled
和onRejected
陆续执行,最后才是setTimeout
被执行。
再考虑以下代码,下面的示例展示了Promise.all
的异步(当传递的可迭代对象是空时,是同步的):
1 | var p1 = Promise.resolve(3); |
可以看出p3
在即使p1
和p2
都是被接受的情况下还是未决议状态。
当script(整体代码)执行完成后,引擎再会去处理p3
的决议(microtask
)。
所有的microtask
都处理完成后,再执行下一个macrotask
(setTimeout)。
resolve和reject只接受一个参数
- 如果使用多个参数,第一个参数之后的所有参数都会被忽略
- 如果没有参数,
value
或reason
则为undefined
没有决议,永远被挂着,不会执行
1 | new Promise((resolve, reject) => { |
被吞掉的异常
以下代码会造成ReferenceError
,然而没有处理。
1 | new Promise(function(resolve, reject) { |
被拒绝的promise
如果没有catch
处理控制台会提示错误信息,但是不影响代码段继续执行,
这就是被吞掉的异常。被吞掉的异常较难察觉,无法记录。因此推荐所有的promise
最后都加上catch
;
1 | new Promise(function(resolve, reject) { |
Promise.all([])
Promise.all
返回一个promise
,只有在传入Promise.all
所有promise
都完成,
返回的promise
才会完成,并且其终值为所有promise
的终值组成的数组。
如果有任何promise
被拒绝,你只会得到第一个拒绝promise
的据因
。
这种模式被称为门
: 只有所有人都到齐,门才会开。
1 | var p1 = Promise.resolve(3); |
Promise.race([])
对于Promise.race
来说,返回的promise
只取决于只有第一个被决议的promise
,
并且其结果和被决议的promise
相同。这种模式被称为门闩
:第一个到达者,打开门闩。
1 | var p1 = Promise.resolve(3); |
异步链式流
then
返回一个promise,可以链式调用:
1 | function getLocation() { |
更复杂的异步链式流
使用Promise
我们能更清晰、更自信把功能拆分,不用担心回调被过多调用
1 | function confirm(content) { |
注意:如果Promise.race
被传入空数组会被挂住,永远不会被决议,而Promise.all
会立即完成。