如果 A 处理函数这里返回了一个 pending 状态的新实例,那么后续所有的链式操作都无法执行;或者返回的是一个 rejected 状态的新实例,那么后续的 B 和 C 也就无法执行了,那居然都不能执行 B 和 C 所在处理程序,那定义来干嘛呢?链式操作就毫无链式可言。又,onRejected 的存在的根本意义无非就是用于捕获 Promise 产生的错误,从而不影响程序的正常执行,所以默认情况下理应返回一个 fulfilled 的新实例。
let p1 = new Promise((resolve, reject) => {
resolve(3)
})
let p2 = p1.finally(() => new Promise(() => {}))
setTimeout(console.log, 0, p2) // Promise {<pending>}
let p3 = p1.finally(() => Promise.reject(6))
setTimeout(console.log, 0, p3) // Promise {<rejected>: 6}
let p4 = p1.finally(() => {
throw new Error('error')
})
setTimeout(console.log, 0, p4) // Promise {<rejected>: Error: error}
执行顺序
先来看一段简单的代码:
new Promise((resolve, reject) => {
console.log('A')
resolve(3)
console.log('B')
}).then(res => {
console.log('C')
})
console.log('D')
// 打印结果:A B D C
上面这串代码的输出顺序是:A B D C。从上面章节介绍的知识点我们知道,executor 执行器会在 new Promise 调用的时候立即同步执行的,所以先后打印 A B 是没问题的。当执行 resolve()/reject() 的时候,会将 Promise 对应的处理程序推入微任务队列,稍等这里提到的对应的处理程序具体是指什么?
new Promise(executor).then(onResolvedA).then(onResolvedD)
function executor(resolve, reject) {
resolve()
}
function onResolvedA() {
console.log('A')
new Promise(executor).then(onResolvedB).then(onResolvedC)
}
function onResolvedB() {
console.log('B')
}
function onResolvedC() {
console.log('C')
}
function onResolvedD() {
console.log('D')
}
执行过程:
执行 new Promise(),立即同步执行 executor 函数,调用 resolve(),此时会将 onResolvedA 推入微任务队列 1,截止目前所有同步代码执行完成;
new Promise((resolve, reject) => {
resolve(1)
}).then(res => {
console.log('A')
}).finally(() => {
console.log('B')
})
new Promise((resolve, reject) => {
resolve(2)
}).then(res => {
console.log('C')
}).finally(() => {
console.log('D')
})
// 打印结果:A C B D
应该很多人会和我当初一样好奇:为什么打印结果不是 A B C D 呢?
这里涉及到一个知识点:如果给 Promise 实例添加了多个处理函数,当实例状态变化的时候,那么执行的过程就是按照添加时的顺序而执行的。
new Promise((resolve, reject) => {
resolve(1)
}).then(onResolvedA).finally(onFinally)
function onResolvedA() {
console.log('A')
}
function onFinally() {
console.log('B')
}
// 打印结果: A B
对于上面这串代码,其实 finally() 处理程序执行的时候已经不是通过 new Promise() 初始化的实例,而是执行完 onResolvedA 函数的时候生成的新实例,不信我们将上面代码中的函数 onResolvedA 稍微改动下:
new Promise((resolve, reject) => {
resolve(1)
}).then(onResolvedA).finally(onFinally)
function onResolvedA() {
console.log('A')
return new Promise(() => {})
}
function onFinally() {
console.log('B')
}
// 打印结果: A
从异步编程说起
我们都知道
JavaScript
的代码执行的时候是跑在单线程上的,可以理解为只能按照代码的出现顺序,从上到下一行一行的执行,但是遇到了异步的行为,比如定时器(一定时间之后才去执行),那就需要等同步代码执行完成后的一段时间里再去执行异步代码。对于同步行为,如下面的代码,我们能够很清楚的知道每一行会发生什么,这是因为后面的指令总是等到前面的指令执行完成后才去执行,所以这里的第二行里的变量
x
在内存里已经是定义过的。但是对于异步代码,我们就不好推断到底什么时候会执行完成了。比如举一个实际的例子,我们去动态加载某个脚本,会这样做:
这个脚本加载完成的时候会去执行定义在脚本里的一些函数,比如初始化函数
init
,那么我们可以会这样写:但是实际执行后却发现,这样根本不行,因为加载脚本是需要花时间的,是一个异步的行为,浏览器执行
JavaScript
的时候并不会等到脚本加载完成的时候再去调用init
函数。以往,对于这种异步编程的做法通常就是通过给函数传递一个回调函数来处理,上面那个例子可以这样做:
上面这样做能够保证在脚本加载完成的时候,再去执行脚本里的函数。但是多考虑一个问题,如果
success
里又需要加载别的js
文件呢,那岂不是需要多层嵌套了。是的,这样的多层嵌套会使得代码层次变得更加深入,难以阅读以及后期维护成本非常高,尤其是当里面加上了很多的判断逻辑的时候情况会更加糟糕,这就是所谓的 “回调地狱”,且又因为它的代码形状很像躺着的金字塔,所以有的人也喜欢叫它 “噩运金字塔”。而为了避免这类 “回调地狱” 问题,目前最好的做法之一就是使用
Promise
。Promise正篇
使用
Promise
可以很好的解决上面提到的 “回调地狱” 问题,直接来看结果:这里通过使用
Promise
实例的then
和catch
函数将多层嵌套的代码改成了同步处理流程,看起来效果还是不错的,那什么是Promise
呢?Promise
首先是一个对象,它通常用于描述现在开始执行,一段时间后才能获得结果的行为(异步行为),内部保存了该异步行为的结果。然后,它还是一个有状态的对象:pending
:待定fulfilled
:兑现,有时候也叫解决(resolved
)rejected
:拒绝一个
Promise
只有这 3 种状态,且状态的转换过程有且仅有 2 种:pending
到fulfilled
pending
到rejected
可以通过如下的
Promise
对象构造器来创建一个Promise
:传递给
new Promise
的是executor
执行器。当Promise
被创建的时候,executor
会立即同步执行。executor
函数里通常做了 2 件事情:初始化一个异步行为和控制状态的最终转换。如上代码所示,
setTimeout
函数用来描述一个异步行为,而resolve
用来改变状态。executor
函数包含 2 个参数,他们都是回调函数,用于控制Promise
的状态转换:resolve
:用来将状态pending
转换成fulfilled
reject
:用来将状态pending
转换成rejected
一个
Promise
的状态一旦被转换过,则无法再变更:可以看到执行了 2 次
resolve
函数和 1 次reject
函数,但是promise
的最终结果是取的第一次resolve
的结果,印证了上面的结论。由
new Promise
构造器返回的Promise
对象具有如下内部属性:PromiseState
:最初是pending
,resolve
被调用的时候变为fulfilled
,或者reject
被调用时会变为rejected
;PromiseResult
:最初是undefined
,resolve(value)
被调用时变为value
,或者在reject(error)
被调用时变为error
。比如上面例子中打印出来的
Promise
对象结果中,fulfilled
是其内部的PromiseState
,而 “第一次 resolve” 是其PromiseResult
。Promise实例方法
Promise.prototype.then()
Promise.prototype.then()
将用于为Promise
实例添加处理程序的函数。它接受 2 个可选的参数:onResolved
:状态由pending
转换成fulfilled
时执行;onRejected
:状态由pending
转换成rejected
时执行。它可以写成这样:
或者写成更简单的方式:
因为状态的变化只有 2 种,所以
onResolved
和onRejected
在执行的时候必定是互斥。上面介绍到了
then()
的参数是可选的,当只有onResolved
的时候可以这样写:当参数只有
onRejected
的时候,需要把第一个参数设置为null
:如果给
then()
函数传递来了非函数参数,则会默认忽略。Promise.prototype.catch()
Promise.prototype.catch()
用于给Promise
对象添加拒绝处理程序。只接受一个参数:onRejected
函数。实际上,下面这两种写法是等效的:Promise.prototype.finally()
Promise.prototype.finally()
用于给Promise
对象添加onFinally
函数,这个函数主要是做一些清理的工作,只有状态变化的时候才会执行该onFinally
函数。因为
onFinally
函数是没有任何参数的,所以在其内部其实并不知道该Promise
的状态是怎么样的。链式调用
链式调用里涉及到的知识点很多,我们不妨先看看下面这道题,你能正确输出其打印顺序嘛?
这里我不给出答案,希望你能动手敲一敲代码,然后思考下为什么?容我讲完这部分知识,相信你能自己理解其中缘由。
从上面这串代码里,我们看到
new Promise
后面接了很多的.then()
处理程序,这个其实就是Promise
的链式调用,那它为什么能链式调用呢?基于onResolved生成一个新的Promise
因为
Promise.prototype.then()
会返回一个新的Promise
,来看下:可以看到
p1
和p2
的内部PromiseResult
是不一样的,说明p2
是一个新的Promise
实例。新产生的
Promise
会基于onResolved
的返回值进行构建,构建的时候其实是把返回值传递给Promise.resolve()
生成的新实例,比如上面那串代码里p1.then(() => 6)
这里的onResolved
函数返回了一个 6 ,所以新的Promise
的内部值会是 6。如果
.then()
没有提供onResolved
这个处理程序,则Promise.resolve()
会基于上一个实例resolve
后的值来初始化一个新的实例:如果
onResolved
处理程序没有返回值,那么返回的新实例的内部值会是undefined
:如果在
onResolved
处理程序里抛出异常,则会返回一个新的rejected
状态的Promise
:基于onRejected生成一个新的Promise
基于
onRejected
的返回值也会返回一个新的Promise
,而且处理逻辑也是一样的,也是通过把返回值传递给Promise.resolve()
产生一个新的实例:这里你会不会有个疑惑?实例
resolve()
的时候,状态由pending
变成rejected
,从而调用onRejected
进行处理,但是为什么有时候会返回一个fulfilled
的新实例呢?试着想一下,如果onRejected
返回了一个pending
的或者rejected
状态的新实例,那后续的链式调用就进行不下去了,看下面例子:如果 A 处理函数这里返回了一个
pending
状态的新实例,那么后续所有的链式操作都无法执行;或者返回的是一个rejected
状态的新实例,那么后续的 B 和 C 也就无法执行了,那居然都不能执行 B 和 C 所在处理程序,那定义来干嘛呢?链式操作就毫无链式可言。又,onRejected
的存在的根本意义无非就是用于捕获Promise
产生的错误,从而不影响程序的正常执行,所以默认情况下理应返回一个fulfilled
的新实例。Promise.prototype.catch()
也会生成一个新的Promise
,其生成规则和onRejected
是一样的。finally生成一个新的Promise
没想到吧,
Promise.prototype.finally()
也能生成一个Promise
。finally
里的操作是和状态无关的,一般用来做后续代码的处理工作,所以finally
一般会原样后传父Promise
,无论父级实例是什么状态。上面说的是一般,但是也有特殊情况,比如
finally
里返回了一个非fulfilled
的Promise
或者抛出了异常的时候,则会返回对应状态的新实例:执行顺序
先来看一段简单的代码:
上面这串代码的输出顺序是:A B D C。从上面章节介绍的知识点我们知道,
executor
执行器会在new Promise
调用的时候立即同步执行的,所以先后打印 A B 是没问题的。当执行resolve()/reject()
的时候,会将Promise
对应的处理程序推入微任务队列,稍等这里提到的对应的处理程序具体是指什么?resolve()
对应.then()
里的第一个入参,即onResolved
函数;reject()
对应.then()
里的第二个入参,即onRejected
函数;或者Promise.prototype.catch()
里的回调函数;所以当执行
resolve(3)
的时候(此时下面定义的这个箭头函数其实就是onResolved
函数),onResolved
函数将被推入微任务队列,然后打印 D,此时所有同步任务执行完成,浏览器会去检查微任务队列,发现存在一个,所以最后会去调用onResolved
函数,打印出 C。其实除了
onResolved
、onRejected
以及Promise.prototype.catch()
里的处理程序外,Promise.prototype.finally()
的处理程序onFinally
也是异步执行的:Promise
链式调用的基础就是因为onResolved
、onRejected
、catch()
的处理程序以及onFinally
会产生一个新的Promise
实例,且又因为他们都是异步执行的,所以在链式调用的时候,对于它们执行顺序会稀里糊涂琢磨不透就是这个原因。题目一
那下面我们就来看点复杂的例子,先来分析下这章开篇提到的题目:
为了方便分析,我们把上面的这串代码写得好看一点:
执行过程:
new Promise()
,立即同步执行executor
函数,调用resolve()
,此时会将onResolvedA
推入微任务队列 1,截止目前所有同步代码执行完成;onResolvedA
函数,打印 A,执行new Promise(executor)
,调用resolve()
函数,此时将onResolvedB
推入微任务队列 2;onResolvedA
函数执行完成。我们知道onResolved
函数会基于返回值生成一个新的Promise
,而onResolvedA
函数没有显示的返回值,所以其返回值为undefined
,那么经过Promise.resolve(undefined)
初始化后会生成一个这样的新实例:Promise {<fulfilled>: undefined}
;由于这个新的实例状态已经变成fulfilled
,所以会立即将其处理函数onResolvedD
推入微任务队列 3;onResolvedB
函数的返回值为undefined
,所以生成了一个resolved
的新实例,则会立即将onResolvedC
推入微任务队列 4;题目二
应该很多人会和我当初一样好奇:为什么打印结果不是 A B C D 呢? 这里涉及到一个知识点:如果给
Promise
实例添加了多个处理函数,当实例状态变化的时候,那么执行的过程就是按照添加时的顺序而执行的。对于上面这串代码,其实
finally()
处理程序执行的时候已经不是通过new Promise()
初始化的实例,而是执行完onResolvedA
函数的时候生成的新实例,不信我们将上面代码中的函数onResolvedA
稍微改动下:由于
onResolvedA
返回了一个这样的Promise {<pending>}
新实例,这个新实例的状态没有发生变化,所以不会执行finally
处理程序onFinally
,所以不会打印 B。这个就说明了,链式调用的时候处理程序的执行是一步一步来的,只要前面的执行完了,生成了新的实例,然后根据新实例的状态变化,才去执行后续的处理程序。所以拿最开始那道题来说:
他的执行过程应该是这样的:
resolve(1)
,将处理程序 A 推入微任务队列 1;resolve(2)
,将处理程序 C 推入微任务队列 2;fulfilled
的新实例,由于新实例状态变化,所以会立即执行finally()
处理程序 B 推入微任务队列 3;题目就先做到这里,相信你和我一样,对
Promise
的执行过程应该有更深入的理解了。接下来我们将继续学习Promise
的相关API
。Promise与错误处理
平时我们写代码遇到错误,都习惯用
try/catch
块来处理,但是对于Promise
产生的错误,用这个是处理不了的,看下面这段代码:从执行结果我们可以看到,报错的信息出现在打印 C 之后,说明抛出错误这个动作是在异步任务中做的,所以
catch
捕获不到该错误就在情理之中了,否则就不会打印 C 了。可见,传统的try/catch
语句并不能捕获Promise
产生的错误,而需要使用onRejected
处理程序:onRejected
捕获了上面抛出的错误后,使得程序正常执行,最后还生成了一个fulfilled
的新实例。除了以上这种直接在
executor
里通过throw
主动抛出一个错误外,还可以通过以下方式产出需要onRejected
处理的错误:注意,如果只是产生了一个错误,却没有抛出来是不会报错的:
Promise
出现了错误就需要使用onRejected
处理程序处理,否则程序就会报错,执行不下去了。Promise API
Promise.resolve()
并非所有的
Promise
的初始状态都是pending
,可以通过Promise.resolve(value)
来初始化一个状态为fulfilled
,值为value
的Promise
实例:这个操作和下面这种创建一个
fulfilled
的Promise
在效果上是一样的:使用这个静态方法,理论上可以把任何一个值转换成
Promise
:这个被转换的值甚至可以是一个
Promise
对象,如果是这样,Promise.resolve
会将其原样输出:Promise.reject()
和
Promise.resolve()
类似,Promise.reject()
会实例化一个rejected
状态的Promise
,且会抛出一个错误,该错误只能通过拒绝处理程序捕获。对于初始化一个
rejected
状态的实例,以下两种写法都可以达到这个目的:与
Promise.resolve()
不同的是,如果给Promise.reject()
传递一个Promise
对象,则这个对象会成为新Promise
的值:Promise.all()
Promise.all(iterable)
用来将多个Promise
实例合成一个新实例。参数必须是一个可迭代对象,通常是数组。可迭代对象里的所有元素都会通过
Promise.resolve()
转成Promise
:所有
Promise
都resolve
后,Promise.all()
才会生成一个fulfilled
的新实例。且新实例的内部值是由所有Promise
解决后的值组成的数组:所有
Promise
中,只要出现一个pending
状态的实例,那么合成的新实例也是pending
状态的:所有
Promise
中,只要出现一个rejected
状态的实例,那么合成的新实例也是rejected
状态的,且新实例的内部值是第一个拒绝Promise
的内部值:Promise.race()
Promise.race(iterable)
会返回一个由所有可迭代实例中第一个fulfilled
或rejected
的实例包装后的新实例。来将上面这串代码变动下:
想要知道
Promise.race()
的结果,无非是要知道到底谁才是第一个状态变化的实例,让我们来具体分析下代码执行过程:resolve(9)
,由new Promise
初始化的实例的状态已经变为了fulfilled
,所以第一个状态变化的实例已经出现了吗?其实并没有,因为迭代第一个元素的代码还没执行完成呢,然后会将return 'A'
所在函数的这段处理程序推入微任务队列 1;reject(6)
,所以由new Promise
初始化的实例的状态已经变为rejected
,由于该实例没有处理函数,所以迭代第二个元素的代码已经全部执行完成,此时,第一个状态变化的实例已经产生;Promise.resolve
将函数返回值 3 转成一个Promise {<fulfilled>: 3}
的新实例,这是第二个状态发生变化的实例;res
,其值是 9,然后处理程序返回了 'A',此时根据之前提到的知识点,这里会新生成一个Promise {<fulfilled>: 'A'}
的实例,这是第三个状态发生变化的实例。此时,第一个迭代元素的代码已经全部执行完成,所以第一个迭代元素最终生成的实例是第三次状态发生变化的这个;p1
已经产生,它是Promise {<rejected>: 6}
,所以会将它的处理程序console.log(err)
所在函数推入微任务队列 2;err
,其值是 6;setTimeout
里的宏任务,打印p1
,至此全部代码执行完成。Promise.allSettled()
Promise.allSettled(iterable)
当所有的实例都已经settled
,即状态变化过了,那么将返回一个新实例,该新实例的内部值是由所有实例的值和状态组合成的数组,数组的每项是由每个实例的状态和内部值组成的对象。只要所有实例中包含一个
pending
状态的实例,那么Promise.allSettled()
的结果为返回一个这样Promise {<pending>}
的实例。对于不支持的浏览器,可以写
polyfill
: