ECMAscript 6 原生提供了 Promise 对象。Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
了解JavaScript事件循环
在解析promise的机制之前,我们需要掌握浏览器的事件循环,此处引用一张事件循环流程图
在上图中的最后2两行队列均为异步队列,可以观察到,每个宏任务队列后都跟随一个微任务队列,
注意,需要把第一个同步任务,也就是首次同步执行的代码视为一个宏任务
在执行栈中的函数遇到异步任务时,会将异步任务交给 右侧相应模块进行处理
第一个队列为宏任务队列,也是主要的异步任务队列,其中的任务由计数器处理模块,ajax请求线程与文件处理模块(NodeJs)进行推入,
第二个队列为微任务队列,在promise对象的then()
方法,与Node中的process.nextTick()
都会在执行时,将传入的回调函数推入当前宏任务后的微任务队列,
当宏任务与微任务相互嵌套时,此时就可能会产生一些异步任务队列与执行栈会相互添加内容,即在栈中异步队列的函数 与 栈中函数产生异步任务,因此形成循环,而JS的异步任务常常通过事件进行执行,也就是为什么这种循环叫做事件循环
Promise 是什么?
抽象的说,Promise是JavaScript中进行异步编程的一个新的解决方案
具体的说,Promise就是一个构造函数,通过函数构造出的实例可以封装一系列异步操作,并可以从中得到异步操作产生的结果数据
为什么要使用Promise?
使用链式调用的编写方式,解决异步回调地狱(代码缩进)的问题
所谓回调地狱,就是当我们要进行多个异步操作的时候,此时一个回调函数包含另一个异步操作,当异步操作数量达到一定程度时,代码会向右持续缩进,这样会使得程序可读性和可维护性下降
1 | // 回调地狱 |
在上面的计时器回调代码中,我们可以看到代码缩进一直在增加,在实际的开发环境中,如果有几十个回调,那么代码就变得非常的不美观,且不易维护
但在promise的链式调用中,只需要将上一个处理的对象使用方法继续进行处理,且捕获异常和错误也比较方便
解决异步任务的最佳办法,是将promise对象配合 ES8 中新增的 async函数与await来操作结果,其中await会等待promise的值,并将之后的语句放入异步任务中执行
Promise/a+ 规范
规范出现的原因:
- 我们不知道异步请求什么时候返回数据,所以我们就需要些回调函数。但是在某些情况下我们需要知道数据是在什么时候返回的,然后进行一些处理。
- 当我们在异步回调里面继续执行异步操作的时候,这样就形成了异步回调函数的嵌套,不好维护以及查找问题
规范的内容是什么?
- 不管进行什么操作都返回一个Promise实例对象,这个对象里面会有一些属性和方法
- 这个Promise实例对象有三种状态
- 一个promise对象只能改变一次状态,而且都会返回一个数据
- pending
- 默认状态
- fulfilled
- 调用
resolve()
返回的状态 - 英文含义为 已完成 ,实际上表示成功的状态
- 返回value
- 调用
- rejected
- 调用
reject()
返回的状态 - 英文含义为 拒绝了 , 实际上表示为失败的状态
- 返回reason
- 调用
- pending
Promise执行机制
Promise的任务执行流程
- 创建一个Promise对象,处于
pending
状态 - 执行异步操作,成功,调用
resolve()
;失败,调用reject()
,都会返回一个新的promise对象 - 执行实例promise.then()方法,按照当前状态,执行内部的回调,可重复步骤2
Promise实例利用闭包,通过调用函数来改变不同的状态,由此来执行对应的回调函数
下列代码中,创建了一个promise对象同时立即改变状态,而跟随其后又连续调用2个then()
,每个then()
调用结束都会默认返回一个新的promise
对象,而then()
中成功/失败的回调内部返回的值则为新promise
的value
,在这一系列调用中如果没有抛出任何异常或者返回任何失败的promise,则都会视为成功的promise
注意:throw xxx 均为失败状态 ,xxx 为reason的值
1 | new Promise(((resolve, reject) => { |
如果想在结果的回调中再执行一个异步任务(也可以说是触发一系列的异步任务),在结果回调函数内需要返回一个新的promise
对象,并成为.then()的返回结果可以在下一次.then()做出更多的操作
1 | new Promise(((resolve, reject) => { |
Promise异常传递
当promise
对象进行一系列任务操作时,在Promise链式调用的回调中,不编写处理失败的回调函数,只在链式调用的末尾使用catch()
处理异常情况,只要遇到了失败状态的promise
,就会将失败的promise逐级传递到最后的catch()
失败处理回调中,如果不抛出异常或者返回失败的promise
对象,.catch()
和.then()
一样,返回成功的promise
1 | new Promise((resolve, reject) => { |
Promise 中断传递
如果因为某个错误,想要在链式调用的过程中中断整个过程,则需要返回一个pending
状态的promise,即返回new Promise(()=> {})
原理:因为then()
内部的回调函数最终是由调用执行器中的resolve()
或者reject()
从而触发执行的,返回一个pending的promise
显然并没有改变任何状态,因为初始化promise
的状态就是pending
1 | new Promise((resolve, reject) => { |
实现 Promise
方法以及规则
在了解了官方Promise的使用方法后,为了实现Promise,我们需要更详细的内部原理与运行机制
- 构造函数
Promise
的方法Promise(executor)
Promise.resolve(value)
Promise.reject(reason)
Promise.all(promiseArray)
Promise.race(promiseArray)
- 实例对象
promise
的方法promise.then(onResolved , onRejected)
promise.catch(onRejected)
- 规则:
- 调用
Promise(executor)
进行初始化promise
时:- 改变状态与值的内部函数
resolve(value)
或者reject(reason)
的调用类型为异步调用,意味着可能等待then()
执行后才进行调用 - 如果存在多次调用
resolve(value)
或者reject(reason)
,则以第一个调用为准,舍弃后续调用 resolve(value)
和reject(reason)
都不执行,而是使用throw xx来抛出错误,需要将promise
状态设置成rejected
- 返回一个
Promise
实例
- 改变状态与值的内部函数
- 调用
promise.then()
时promise.then()
应该返回一个新的promise
对象- 如果
promise
的状态为pending,那么将其推入实例的回调函数数组中 - 如果
promise
的状态为fulfilled或者rejected,那么将传入的回调立即作为异步函数执行 - 如果在规则2.3满足的情况下,回调函数返回了一个
promise
对象,那么,promise.then()
返回的新promise
对象为回调函数返回的对象 promise.then()
能够实现传透的功能,即在不传入失败的回调函数onRejected
或者成功的回调函数onResloved
时,使用then()
的链式调用能够将失败的promise
往后传递
- 调用
Promise.resolve(value)
时,value
可以为promise
类型,也可以为非promise
类型,调用后返回一个成功/失败的promise
- 调用
Promise.reject(reason)
时,reason
只能是非promise
类型的值 - 调用
Promise.all(promiseArray)
应该在所有promise均为成功的情况下,返回一个值为所有promise
的值的数组的新的成功的promise
,否则,返回第一个失败的promise
- 调用
Promise.race(promiseArray)
应该返回数组中第一个改变状态的promise
- 调用
Promise构造函数
Promise主构造函数,接收一个函数作为执行器,执行后立即产生一个promise
对象
- 构造函数内部拥有三个属性
- state:保存状态
- data:保存数据
- callbacks:保存一个
then()
方法添加的回调函数的对象
- 在
resolve()
和reject()
对this.state
和this.data
的改变后,需要符合规则1.1和1.2:即将对应回调函数推入异步任务队列中 - 在
resolve()
和reject()
都没有被调用的情况下,需要符合规则1.3 : 即使用捕获来处理异常情况 - 在executor中调用
resolve()
或者reject()
,其本质是同步/异步地改变状态和值(多数情况下为同步),然后将promise
对象内部的callbacks 中对应的成功或者回调放置在异步队列中进行调用,可能此时callbacks没有回调函数,但是promise.then()
是同步的将回调放入自身callbacks中,即放入回调函数这一操作永远比在异步队列中执行回调函数先执行,这就是为什么Promise的链式调用能够处理异步请求的原因
1 | const PENDING = 'pending' |
值得注意的是,如果使用function
声明的resolve()
和reject()
函数,那么内部的this
就是指向的window
,因为这两个函数被外部传入的箭头函数executor
调用,而外部的executor的this
指向的是window
,那么内部的两个函数就是被windows
调用,将resolve()
和reject()
改为箭头函数声明,即在声明时就执行父级作用域的this
,即new
语句产生的promise
对象
Promise.prototype.then ()
给Promise
主构造函数原型上添加then方法 ,主要用于实例promise
then()
方法主要是用于同步的将传入的回调函数放入promise
的callbacks属性中、或者根据promise的状态调用响应的回调函数,最后返回一个新的promise
对象_proimise
的状态为pending ,那么直接将回调推入callbacks属性中,那么此时构造_proimise
的执行器中的resolve()
或者reject()
还没有被执行,也就是说,在状态还没出现之前,就调用了then()
,那么如何在后面异步任务中产生了状态之后就可以马上执行前面then()
中的回调以及返回新的promise
对象呢?在
resolve()
或者reject()
被调用的时候也会将回调函数数组中的函数加入异步队列中等待调用,而等待到这些函数被调用的时候,即是返回的promise
对象获得状态的时候,而传入的回调onResolved
和onRejected
也有三种情况:返回promise
,返回非promise
,抛出异常,使用try catch
以及递归进行处理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
29if (this.state === PENDING) {
this.callbacks.push({
// 当在resolve调用的时候,这里的onResolved会被加入异步队列并被调用,同时也使得返回的promise拥有了状态和值
onResolved (value) {
try {
let result = onResolved(value)
if (result instanceof Promise) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
},
onRejected (reason) {
try {
let result = onRejected(reason)
if (result instanceof Promise) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
}
})
}
promise
的状态为resolve或者reject,此时异步队列中已经没有执行的回调,需要在接收到回调的时候将其放入异步队列中,即使用计时器,在计时器异步任务中执行对应状态的回调函数如果状态回调函数返回的是非_Promise类的值,也就是说手动调用
onResolved
/onRejected
后返回的值不是Promise
类型,那么返回的新对象就在执行器中直接调用resolve(result)
来将返回的promise对象设置为 成功的状态(只要不抛出错误且不返回promise
的回调均为成功)状态回调函数内部使用
throw
抛出了异常或者普通值,那么在catch(error){}
中直接将要返回的promise的状态设置为rejected,且值为error状态回调函数内部返回了一个
promise
对象,此时需要将这个对象特殊处理首先整个
then()
方法的参数接收2个函数参数,返回一个新promise
对象,其中构造函数执行器为同步执行,所以将判断代码放入执行器中判断调用
then()
方法的本体promise
的状态,其中最简单的如果是pending状态则就仅仅将回调函数放入返回的新promise
对象的回调数组中,然后将在异步任务中被调用,这次情况就符合第一次创建promise
对象时,执行器中的resolve()
或者rejected()
在异步任务中的情况如果调用
then()
方法的本体promise
的状态为fulfilled或者rejected , 此时需要对应状态使用计时器创建一个新的异步任务,并在这个异步任务中调用.then()传入的回调函数,并拿到回调函数返回的结果,通过结果来进一步生成新的promise
作为then()
的返回结果
那么此时就会遇到一个问题:如何去继承回调中返回的 promise的状态和值呢?如何判断这个promise的状态是成功还是失败呢?
then()
方法就是用来判断调用的promise的成功与失败的方法,那么我们可以then()
内的回调中,即返回的promise
内也对作为promise
对象的结果,调用一次then()
,并在其中传入,要返回的新promise
的状态设置函数resolve(value)
和reject(reason)
,这样就可以将后续promise
的状态与最终值继承到第一次调用then
返回的promise
对象中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19Promise.prototype.then = function (onResolved, onRejected) {
var _this = this
// ...
return new Promise(function (resolve, reject) {
var handleCallback = function (callback) {
try {
var result = callback(_this.data) // 将结果计算出来
if (result instanceof Promise) {
result.then(resolve, reject) // 将上方new Promise的resolve、reject作为结果的promise.then()的回调
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
}
})
}在此可以举一个例子:
假如说现在创建了一个
Promise
的实例对象p
,其状态为fulfilled ,值为11
然后调用了
p.then()
,其中成功的回调返回了一个新的Promise
实例对象,且状态为成功,值为22
1
p.then(value => new Promise((resolve,reject) => {resolve(22)}))
接着
p.then()
调用后,会返回一个新的Promise
实例对象,我们来看这个新的Promise
实例对象是如何产生的- 按照
then()
的代码进入return
右侧new Promise()
的执行器中,在这里,我们将这个new Promise()
最后所构造出来的对象称为p1
- 此时的
this
为最前面的实例对象p
,对p
的状态进行判断,执行转到else if (this.state === RESOLVED) {...}
中,其内部为一整个异步任务,被主线程放到异步队列中等待执行,此时p.then()
的同步任务就执行完毕了 - 假设同步任务已经执行完毕,现在跳转到异步任务中执行,也就是开始执行
setTimeout
中回调的内容,异步任务开始 - 在
try catch
中,p.then()
内部的函数将p.data
作为参数放入传入的value=> ...
箭头函数中执行,此时的onResolved(this.data)
就等于带着参数执行了value => new Promise((resolve,reject) => {resolve(22)}
- 此时判断result,发现返回了一个
Promise
的实例对象,这里简称result
,其状态为fulfilled
,值为22,那么调用这个实例对象result.then()
,且成功、失败的的回调函数分别为p1
的resolve(value)
和reject(reason)
- 进入到
result.then()
中,同样又进入一个新promise的创建流程,进入执行器,判断result
的状态,上方我们设置的为成功的状态,那么在同步队列执行完的异步队列中同样执行了let result = onResolved(this.data)
这一句,且此时的onResolved(this.data)
就是p1构造器中的resolve(value)
,调用之后,此时p1的对返回的promise
对象的this.data
继承已经完成了 - 这个时候构造p1的异步任务已经完成了,此时通过return返回,成为
p.then()
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
63
64
65
66
67
68
69
70Promise.prototype.then = function (onResolved , onRejected) {
// 始终返回一个新的_promise,需要判断返回的状态,以及回调返回的值,
// 将判断任务放入执行器中执行,以便产生不同状态的promise
return new Promise((resolve, reject) => {
// 如果是无状态的,那么直接将回调放入属性中
if (this.state === PENDING) {
this.callbacks.push({
onResolved (value) {
try {
let result = onResolved(value)
if (result instanceof Promise) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
},
onRejected (reason) {
try {
let result = onRejected(reason)
if (result instanceof Promise) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
}
})
} else if (this.state === RESOLVED) {
// 如果这个_promise对象有状态,则将其回调放入异步队列中执行,并判断其执行后的返回值是否为_Promise类型
// 如果是,则需要使用自身的.then来判断在其手动创建的_promise的执行器到底是调用了那个状态函数
setTimeout(() => {
try {
let result = onResolved(this.data)
if (result instanceof Promise){
// result.then(
// 如果手动返回的_promise的执行器中调用的是resolve(),那么则会执行
// 当成功/失败时,将原来的.then()对象也为成功/失败,并且继承回调内部返回的_promise的值
//value => resolve(value),
//reason => reject(reason)
//)
result.then(resolve, reject)
} else {
resolve(result)
}
}catch (error) {
reject(error)
}
})
} else {
setTimeout(() => {
try {
let result = onRejected(this.data)
if (result instanceof Promise){
result.then(resolve, reject)
} else {
resolve(result)
}
}catch (error) {
reject(error)
}
})
}
})
}此时会发现,上面的代码
try catch
部分重复了4次,所以将重复代码提取为一个新函数,并增加异常传透的功能- 按照
1 | Promise.prototype.then = function (onResolved, onRejected) { |
这时候就有一个问题:为什么调用.then()的有状态的promise时候,内部的handleCall(onResolved)
要使用异步调用呢?
这是因为,状态主要是依靠构造promise时执行器中的resolve()
和reject()
两个函数进行的,而先有回调函数再有状态的原因是在构造器内部使用了计时器调用了resolve()
和reject()
,导致同步的then()
先执行,将内部传入的回调放进了promise
对象的callbacks中
但先有状态,后有回调的情况就是:先执行了构造器的所有内容,立即改变了状态以及值,将执行自身的回调函数这一任务放进了计时器,从而变为了在同步任务结束之后的异步任务;接着在结束promise
的构造后,立即调用了then()
但进入了.then()后,有状态的promise则不会像无状态的一样,直接放入promise的callbacks,来等待resolve()
和reject()
,因为已经在执行器里面同步执行过了,这个时候.then()要根据调用回调函数的结果,返回一个新的promise
,而这一过程往往是应用于发送异步请求,如果在此处直接调用handleCall(onResolved)
,那么主线程就会等待这个then()
中回调函数产生的结果从而去创建新promise(包括改变它的状态与值)这一过程,这就违背了Promise思想的初衷
还有一点是,异步的handleCall(onResolved)
与同步创建promise
的执行器形成了闭包,这样,handleCall(onResolved)
在异步任务中执行时,还可以去改变同步任务时创建的状态为pending的promise
对象,然后通过微任务队列,先调用then()
的先执行,先改变then()
返回的promise的状态,这样就形成了一种前后关系
Promise.prototype.catch()
catch()
其实就是then()
处理失败的情况,传入回调到onRejected
调用并返回then()
的结果即可,其中onResolved
的位置设置为undefined
或者null
都可以
1 | Promise.prototype.catch = function (onRejected) { |
Promise.resolve(value)
Promise.resolve(value)
返回一个成功的promise
,值为value
,value
的值可以普通值,也可以是promise
对象
1 | // .reslove()返回一个新的promise对象,并且需要判断value的类型 |
Promise.reject(reason)
Promise.reject(reason)
返回一个失败的promise
,值为reason
,reason
的值只能为非promise
类型的值
1 | // .reject()方法也是返回一个promise对象,并且只接受一个reason |
Promise.all(promiseArray)
Promise.all(promiseArray)
返回一个promise
,接收一个参数promiseArray
(_promise数组),但其中的元素可以是promise
对象,也可以是非promise
对象
当内部所有promise的状态都为成功的时候,返回成功的promise,且promise的value为一个数组,其值promiseArray所有promise的value
否则返回失败的
promise
且其value为状态为失败的promise
的value
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
27Promise.all = function (promiseArray) {
if (promiseArray instanceof Array) {
return new Promise((resolve, reject) => {
const valueArray = new Array(promiseArray.length)
let counter = 0
// 遍历promise数组使用then判断状态,并在回调中执行值的积累和以及改变状态
promiseArray.forEach((promise, idx) => {
// 无论数组中的值是否为promise类型,使用.resolve()将其强制转换为promise对象,再进行.then()的判断
Promise.resolve(promise).then(
value => {
counter++
valueArray[idx] = value
if (counter === promiseArray.length) {
resolve(valueArray)
}
},
reason => {
reject(reason)
}
)
})
})
} else {
return null
}
}valueArray[idx] = value 这行为什么不能被 valueArray.push(value )代替呢?
此处不使用push进行增加元素的原因是因为传入的promise在构造时,可能resolve()或者reject()延迟调用的情况,如果使用了索引,那么在同步遍历时,就以闭包的形式锁定了value在数组中的位置
Promise.race(promiseArray)
Promise.race(promiseArray)
返回一个promise
,接收一个参数promiseArray
(promise数组)
- 返回数组中,最快产生成功或者失败状态的
promise
- 例如:[p1,p2,3] ,p1延迟2秒更新状态, p2 为同步获得状态,3为非promise,那么将数组看做为一个队列,p2和3为最先获得状态,但p2排在3的前面,所以最终结果输出p2
1 | Promise.race = function (promiseArr) { |
使用微任务
到此你会发现,当promise调用回调函数的时候,总是使用的setTimeout()
来将其模拟为异步任务来调用,这个时候就出现了一个问题:
如果在所有创建promise
对象之前就启动一个0秒的定时器来调用输出,那么这个定时器内的内容一定是最先输出的(宏任务队列)这明显不符合官方Promise
的一个特点:始终在异步微任务中调用回调函数;
通过文档了解到所有定时器函数、ajax请求、DOM事件回调均为宏任务,而因为使用了定时器,以上实现的promise并没有启动任何微任务,那么如何将一个回调函数变成在微任务中执行呢?
这个问题可以在使用CommandJs
模块规范的NodeJs中使用process.nextTick(callback)
解决,process.nextTick(callback)
在NodeJs中用于将回调函数加入微任务队列,由此就可以真正模拟官方Promise
的功能
以下是Promise
构造函数 以及then()
方法的的最终代码
1 | function Promise (executor) { |
ES6 class(类)版本的 Promise
新增 Promise.any(array)
方法,其目的与Promise.all(array)
方法相反:只要数组中出现成功的状态的promise
,立即返回成功状态的promise
1 | const FULFILLED = 'fulfilled' |
Promise面试题
1 | setTimeout(()=>{ |
让我们从头开始解析:
- 首先第一句执行
console.log(0)
的计时器进入宏队列 - 主线程同步任务到达第一个new,创建第一个promise ,执行内部的
console.log(1)
,然后执行then()
,将console.log(2)
以下直到console.log(6)
以上的放入微任务队列 - 主线程同步任务到达
console.log(6)
下方的new,创建第二个promise,执行console.log(7)
,然后执行then()
将console.log(8)
放入微任务队列 - 主线程同步任务执行完毕,开始执行异步任务
- 首先执行异步微任务队列
- 微任务队列中的第一个微任务,包含
console.log(2)
的.then()
代码块被执行,接着进入console.log(2)
下方的new中执行console.log(3)
,以及resolve()
,再执行.then()
,此时将调用console.log(4)
的回调放入微任务队列末尾 - 此时第一个
promise.then()
中的代码已经执行完毕了,即产生了结果,所以他的下一个then()
被执行,console.log(6)
被加入微任务队列 - 接下来执行微任务队列中的第二个异步微任务,即第二个
promise.then()
中的console.log(8)
- 接着处理微任务队列中的第三个异步微任务,即第6步放入的任务,执行完毕后,执行下一个
then()
,console.log(5)
的回调被放入微任务队列 - 接着处理微任务队列中的第四个异步微任务,即第7步被放入的调用
console.log(6)
的回调 - 最后执行微任务队列中最后一个任务,即第9步中放入的调用
console.log(5)
的回调 - 此时微任务队列中的任务已经全部执行完毕,开始转到宏任务队列中,执行下一个宏任务
- 第1步中的计时器
console.log(0)
被执行,整个过程执行完毕 - 整体输出 1 7 2 3 8 4 6 5 0
Promise总结
最后,简单的总结一下Promise主要的的工作过程
- 每个
promise
对象都拥有一个回调容器,包含成功与失败的回调函数,他们在promise
本身被创建的时候为空 promise
处理异步任务关键是在于在创建promise
时,内部的状态改变函数一般为同步执行,改变立即其状态以及值,但同时将执行自身的回调函数放入异步微任务队列中- then方法的过程分为两种情况
- 第一种情况是当
promise
无状态时,将收到的函数立即推入promise
的回调容器中(),等待主线程处理微任务队列 - 第二种情况是当
promise
有状态时,直接将回调函数调用推入异步微队列,并立即返回一个新的promise
(此时状态暂时为pending
),利用JS的闭包,将改变这个返回的promise
对象状态的函数放入微任务中的回调内,这样做就可以在异步微任务中更新立即返回的promise
的状态
- 第一种情况是当