写在前面
可以说现在是promise遍天下,谁还没写过promise呢!
我们都知道JS分为同步任务和异步任务,而异步任务又分为宏任务和微任务。
其中promise就是我们所说的微任务中的一种。本文只探讨promise的执行机制,希望你不再为它的一些行为所困扰。
先通过几个小案例来看一下,它们是否符合你的预期呢?
经典案例
案例一:
setTimeout(() => {
console.log(1)
})
new Promise((rs, rj) => {
console.log(2)
rs()
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
Promise.resolve()
.then(() => {
console.log(5)
})
console.log(6)
输出结果顺序:2,6,3,5,4,1
案例二:
let p1 = new Promise((rs, rj) => {
console.log(1)
setTimeout(() => {
rs()
})
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
})
setTimeout(() => {
console.log(4)
})
Promise.resolve()
.then(() => {
console.log(5)
})
p1.then(() => {
console.log(6)
})
console.log(7)
输出结果顺序:1,7,5,2,3,6,4
案例三:
Promise.resolve(1)
.then(2)
.then(
Promise.resolve(3)
)
.then(
Promise.resolve(4, console.log(5))
.then(r1 => {
console.log(r1)
})
)
.then((r2 => {
console.log(r2)
})())
.then(console.log)
console.log(6)
输出结果顺序:5,undefined,6,4,1
结果说明
由案例一能够看出先执行同步任务2,6,再执行微任务3,5,4,最后执行宏任务1。
其实js中所有代码的执行,都遵循这个顺序,同步任务 -> 微任务 -> 宏任务。只不过在实际运行过程中将任务推入执行队列的顺序不同,而产生了各种不同的效果。
由案例二能够看出1,7都是同步任务先执行,再执行微任务5,然后开启下次事件循环,执行第一个宏任务,然后在该次事件循环结束之前执行微任务2,3,6,最后开启下次事件循环执行宏任务4。
其实js中所有的事件循环都可以看成是一次次的宏任务,也就是说宏任务总是在下次事件循环的开始后执行,微任务总是在本次事件循环阶段的结束前执行,如果在本次事件循环完成之前一直追加微任务,那么就会不断的执行微任务队列,直到清空,而本次事件循环添加的微任务,会在之后的事件循环中执行。
由案例三能够看出Promise.resolve属于同步任务,then中的参数会作为表达式进行求值之后再放入微任务队列或者缓存中,对于then参数为非函数的表达式不会作为结果传递给下一个then。
其实js中函数在进行参数传递的时候,会先进行求值,然后再以值的形式传入调用的函数。因此会先执行同步任务的5,undefined(第5行代码),6,然后再执行微任务4,1(第6行代码)。
原理分析
由于Promise存在三种状态pending、fulfilled和rejected。其中pending为等待状态,将来可能发生改变,而以微任务的执行顺序来说fulfilled和rejected都是同样的机制,因此下文均以fulfilled状态来讨论。
抛开同步任务最先按顺序执行不说,代码每执行到异步任务(宏任务和微任务)的时候,都是按照入栈出栈的操作,即按照先入先出的规则来调配的。
我们现在清楚几点概念:
- new Promise是同步任务
- Promise.resolve是同步任务
- then参数的表达式求值是同步任务
- then参数为函数才会被推入微任务队列
- 只有前一个then执行完毕,下一个then才会被推入微任务队列,等待执行
- then中如果返回值为promise,那么会分为两次推入微任务队列
(可以理解为返回实际结果之前又执行了两个子微任务。第一次子微任务把return后面的结果以微任务形式推入执行队列,第二次把第一次子微任务得到的结果用then再次推入执行队列以供将来把值返回给主微任务)
我们用几个案例来说明上述情况:
//new Promise中的回调函数以同步方式执行
//只有状态为fulfilled才会将接下来的任务推入微任务队列
new Promise((rs, rj) => {
console.log(1)
rs()
})
console.log(2)
//输出1,2
//Promise.resolve中的参数表达式是以同步的方式进行求值
//只不过该状态的完成永远为fulfilled
//而Promise.reject的完成状态永远为rejected
let a = 1
Promise.resolve(a = 1 + 2)
console.log(a)
//输出3
/* 可以把 Promise.resolve(value)
等价看成是 new Promise((rs,rj) => {
rs(value)
}) */
//会以同步的方式扫描每一个then的参数,并将表达式求值
//只有上一个状态为fulfilled才会将该then的回调函数放入微任务队列
//否则将保存调用关系到缓存堆中,以供将来执行
new Promise((rs, rj) => {
rs(1)
}).then(() => {
return 2
}).then(((r) => {
console.log(r)
return 3
})(4))
console.log(5)
//输出4,5
//只要then的参数表达式求值结果为函数,那么就会被当做待执行的回调
let a = null
new Promise((rs, rj) => {
rs(5)
}).then((a = 1,(r) => {
console.log(r)
return 2
})).then((() => {
return () => {
console.log(3)
}
})())
console.log(a)
//输出1,5,3
//第8行并不是打印一个Promise函数
//而是等待上一个then返回值的状态完成之后resolve回来的值
new Promise((rs, rj) => {
rs(1)
}).then(() => {
return myP()
}).then((r) => {
console.log(r)
})
console.log(2)
function myP() {
return new Promise((rs, rj) => {
console.log(3)
setTimeout(() => {
rs(4)
},1000)
})
}
//输出2,3,4(其中4是一秒之后才打印的)
//当then回调中遇到return一个Promise时
//会执行两次子微任务,然后再执行后面的then回调
new Promise((rs, rj) => {
rs(1)
}).then(() => {
console.log(2)
//输出同步执行的8之后,会把结果第一次以子微任务的形式(记为P1)推入微任务队列
//输出3之后,会执行P1这个微任务,然后再把结果用then(记为T1)推入微任务队列
//输出4之后,会执行T1这个微任务,再把后面的主微任务的then推入微任务队列
//这样输出5之后就会输出9
return myP()
}).then((r) => {
console.log(r)
})
new Promise((rs, rj) => {
rs(3)
}).then((r) => {
console.log(r)
return 4
}).then((r) => {
console.log(r)
return 5
}).then((r) => {
console.log(r)
return 6
}).then((r) => {
console.log(r)
})
console.log(7)
function myP() {
return new Promise((rs, rj) => {
console.log(8)
rs(9)
})
}
//输出7,2,8,3,4,5,9,6
因此,我们现在可以对上面的三个案例进行分析,很容易得出它们的执行结果:(我们约定宏任务队列用macroTask来表示,微任务队列用microTask来表示,缓存堆用cacheStack来表示。)
第一段属于宏任务,直接推入宏任务队列,因此macroTask:[第一段]
第二段属于同步任务,直接执行输出2
,遇到rs状态变为fulfilled,将第三段推入微任务队列,因此microTask:[第三段]
由于第三段还未执行,处于pending状态,所以将第四段与第三段建立关系之后,推入缓存堆
第五段属于同步任务,并且直接处于fulfilled状态,将第六段推入微任务队列,因此microTask:[第三段,第六段]
第七段属于同步任务,直接输出6
,到此同步任务执行结束
执行微任务队列,执行第三段输出3
,状态变为fulfilled,将第四段推入微任务队列,因此microTask:[第六段,第四段]
继续执行微任务队列,执行第六段输出5
,因此microTask:[第四段]
继续执行微任务队列,执行第四段输出4
,因此microTask:[]
微任务执行完毕,本次事件循环结束,进入下一个事件循环
执行宏任务,执行第一段输出1
,因此macroTask:[]
到此所有任务队列执行完毕。
第一段属于同步任务,直接执行输出1
第二段属于宏任务,直接推入宏任务队列,因此macroTask:[第二段]
由于未执行rs,当前promise处于pending状态,所以将第三段与第一段建立关系之后,推入缓存堆,因此cacheStack:{第一段→第三段}
同样由于第三段还未执行,处于pending状态,所以将第四段与第三段建立关系之后,推入缓存堆,因此cacheStack:{第一段→第三段,第三段→第四段}
第五段属于宏任务,直接推入宏任务队列,因此macroTask:[第二段,第五段]
第六段属于同步任务,并且直接处于fulfilled状态,将第七段推入微任务队列,因此microTask:[第七段]
第八段属于微任务,由于p1处于pending状态,所以将第八段与第四段建立关系之后,推入缓存堆,因此cacheStack:{第一段→第三段,第三段→第四段,第四段→第八段}
第九段属于同步任务,直接执行输出7
,到此同步任务执行结束
执行微任务队列,执行第七段输出5
,状态变为fulfilled,因此microTask:[]
微任务执行完毕,本次事件循环结束,进入下一个事件循环
执行宏任务,执行第二段,因此macroTask:[第五段],并且将第一段状态变为fulfilled,将第三段从缓存堆中取出并推入微任务队列,因此microTask:[第三段]
执行微任务,执行第三段输出2
,状态变为fulfilled,将第四段从缓存堆中取出并推入微任务队列,因此microTask:[第四段]
执行微任务,执行第四段输出3
,状态变为fulfilled,将第八段从缓存堆中取出并推入微任务队列,因此microTask:[第八段]
执行微任务,执行第八段输出6
,因此microTask:[]
微任务执行完毕,本次事件循环结束,进入下一个事件循环
执行宏任务,执行第五段输出4
,因此macroTask:[]
到此所有任务队列执行完毕。
第一段属于同步任务,并且直接处于fulfilled状态
第二段中then的参数为数值2,不是函数,因此只进行求值
第三段中then的参数为Promise对象,不是函数,因此只进行求值
直接执行第四段的同步任务代码
第五段中then的参数同样为Promise对象,不是函数,因为只进行求值
直接执行第六段代码,由于Promise.resolve属于同步任务,并且会对参数先求值再进行传递,因此直接输出5
,状态变为fulfilled,将第七段推入微任务队列,因此microTask:[第七段]
第八段中then的参数求值之后为立即执行函数返回的undefined,不是函数,由于没有传递参数r2,因此直接输出undefined
第九段由于前面的第一段状态为fulfilled,所以直接推入微任务队列,因此microTask:[第七段,第九段]
第十段属于同步任务,因此直接执行输出6
,到此同步任务执行结束
执行微任务队列,执行第七段,由于第六段返回的值为4,所以输出4
,因此microTask:[第九段]
继续执行微任务队列,执行第九段,由于第一段返回的值为1,所以输出1
,因此microTask:[]
到此所有任务队列执行完毕。
好了,上面的三个案例已经全部讲解完毕了,只要跟着读一遍,就会感到非常清晰了。
写在最后
Promise的出现为大家带来了方便,解决了很多的麻烦事,但是也为大家带来了一些困惑,尤其是执行时机上面的疑问。不过我们在日常开发中不必过于纠结,除了同步任务之外,我们尽量不要在宏任务或者微任务上面有任何执行顺序方面的假设,都把他们当做是异步任务就可以了,尤其generator与await的出现,我们一般都是多种相结合来实现功能需求的。
希望对你有所帮助,做一个快乐的码农~[吐舌]