轻松拿捏Prmise的执行顺序,深入理解规则
看完这篇文章,再也不怕问你Promise执行顺序啦!
1.嵌套的Promise
1.1无返回值的Promise
我们先来看一下下面的代码,看看执行顺序是什么
let p1 = new Promise((resolve, reject) => {
//Promise1
console.log('---1');
resolve(1);
}).then(() => {
//回调 A
console.log('---2');
new Promise((resolve, reject) => {
//Promise2
console.log('---4')
resolve(2);
}).then(() => {
//回调C
console.log('---5');
}).then(() => {
//回调D
console.log('---6');
})
}).then(() => {
//回调B
console.log('---3');
})
公布答案:
1 2 4 5 3 6
为了更好的分析,读者可以先读一下法则(笔者根据规范总结)和一些定义。
前置定义:处理回调的方法意思是:运行该回调并对回调的返回值进行分析处理,下面称为handler
法则1:!!微任务队列不能立刻执行,得等待系统调度执行!!(有关宏任务,微任务知识的本文默认有该基础)
法则2:我们这么想象每个promise有两个数组,一个onFulfilledCallbacks
(成功的回调),一个onRejectCallbacks
(失败时的回调);当promise状态改变时,如果resolve,状态变更为fulfilled
,将遍历onFulfilledCallbacks
数组,把里面的handler推入微任务队列中,如果reject,状态变更为rejected
.则将遍历onRejectCallbacks
数组,把里面的handler推入微任务队列中。
法则3:当遇到then时,会先检查当前产生这个then
的promise的状态,如果状态为pending
,则将then(onFulfilled,onRejected)
的入参onFulfilled
和onRejected
裹挟在处理方法handler里面,分别push进Promise自身内存空间onFulfilledCallbacks
和onRejectCallbacks
中;如果状态为fulfilled
或者rejected
,则直接将处理该回调onFulfilled
或者onRejected
的方法handler推进微任务队列中。 PS注意,别混淆,!!then里面的回调是在微任务队列运行的,但解析该then函数是直接依次直接解析的!!
法则4:then链式调用时,必须等上一个then内部的promise状态改变(运行完then的回调函数得到结果x
,如果x
不为promsie实例,则状态变更为resolved或rejected;如果x为promise实例,则递归等待X状态改变 ,这也是handler的逻辑) 才能继续执行,同时必须返回一个Promsie。章节1.2会细讲。
好的,法则列出完毕。做对了吗?话不多说,下面笔者展示一下分析过程
首先代码遇到new Promise
,根据MDN文档描述,
executor 是同步调用的(在构造 Promise 时立即调用),并将 resolveFunc 和 rejectFunc 函数作为传入参数。
所以直接运行里面的构造函数的参数executor
,也就是(resolve,reject)=>{....}
,就先执行console.log('---1')
。
接着我们看到直接执行了resolve(1)
,我们知道当执行resolve()
函数时,所在的Promise状态将改变为已解决(resolved)
,也就是从pending
变成fulfilled
,根据法则2,遍历自己的onFulfilledCallbacks
,这时发现里面没东西,于是过。这里有些同学就有疑问了,后面的then不是回调吗,怎么不执行,哎哎哎,童鞋你先别急,我代码都还在resolve(1)
这里,都没运行到后面的then呢,当然为空数组;
接着终于来到第一个then
了,我们暂且称为回调A
,根据法则3,发现产生该then
的promise为Promise1
,它的状态已经resolved变成fulfilled
,直接将处理该回调A(也就是中间那一坨)的方法handlerA推入微任务队列,等待执行;接着代码来到下一个then,也就是回调B,根据法则3,检查产生该then的promsie也就是回调A所在的then返回的Promise,暂为PromiseA(法则4:then须返回promise)的状态,首先它是没有自定义返回值的( 自己没有写return,只要不return promise实例就行 ),那么现在有返回值了吗?答案是没有,因为法则4说需要等待then里面回调的结果(此处指回调A)才判断状态改变,很明显,handlerA还在微任务队列里面,都没运行,所以此时状态的pending
,将handlerB推入PromiseA自身的onFulfilledCallbacks
中,等待handlerA执行完再加入微任务队列。
好,现在从头到尾解析完了,没啥事了,然后系统就查看微任务队列,发现里面有个handlerA,刷刷执行,就到了console.log('---2')
,接着发现new Promsie
,暂时称为promise2,然后直接执行executor,就console.log('---4')
,接着遇到resolve(2)
,然后该promsie2的状态变更为fulfulled
,根据法则2,遍历自己(promise2)的onFulfilledCallbacks
,这时发现里面没东西,继续!就执行到回调所在的then
,根据法则3,发现promsie2的状态为fulfilled
,直接将该处理回调C的方法handlerC推入微任务队列,等待执行,接着碰到回调D所在的then,那就看回调C所在的then返回的promsie(暂为promiseC)状态是否改变,同理根据法则4,handlerC还在微任务队列中没运行,说明决定处理回调D的方法handlerD推入微任务时机的promise状态为pending
,将handlerD推入promiseC的onFulfilledCallbacks
中,等待promsieC状态改变。
分析一下场上状态。在Promise内存中,在PromiseA的内存队列onFulfilledCallbacks
存放着handlerB,在PromiseC的onFulfilledCallbacks
存放着handlerD;在微任务队列中,存在着handlerC。
此时回调A总算运行完啦,返回值为undefined
,根据法则4,返回值x
为undefined
,则PromsieA状态变更为fulfilled
,根据法则2,遍历自身缓存队列将handlerB推入微任务队列中,这时候微任务队列存在 handlerC和handlerB。接着系统发现没啥事了,遍历微任务队列依次执行,先执行handlerC,console.log('---5')
,执行完C返回值undefined
,在handlerC内部将PromiseC状态变更为fulfulled
,接着遍历自身缓存队列将handlerD推入微任务队列中;接着执行微任务handlerB,console.log('---3')
;然后执行微任务handlerD
,console.log('---6')
,至此结束。
1.2 有return的Promsie
我们对上面代码稍微做一下改造,在回调A中加个return,上述代码顺序是否会改变呢?
let p1 = new Promise((resolve, reject) => {
//Promise1
console.log('---1');
resolve(1);
}).then(() => {
//回调 A
console.log('---2');
return new Promise((resolve, reject) => {
//Promise2
console.log('---4')
resolve(2);
}).then(() => {
//回调C
console.log('---5');
}).then(() => {
//回调D
console.log('---6');
})
}).then(() => {
//回调B
console.log('---3');
})
公布结果:变了,
1 2 4 5 6 3
有童鞋好奇,加了个return ,为什么3会最后才执行;这一切都是法则4的魔力,根据promiseA+规范 2.2.7,
then 方法必须返回一个 promise 对象,promise2 = promise1.then(onFulfilled, onRejected); 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 promise 解决过程:
[[Resolve]](promise2, x)
和规范2.3.1与2.3.2
2.3.1 如果 x 为 promise ,则使 promise 接受 x 的状态 2.3.2 如果 x 为 promise ,则使 promise 接受 x 的状态: 2.3.2.1如果 x 处于等待态, promise 需保持为等待态直至 x 被完成或拒绝 2.3.2.2如果 x 处于完成态,用相同的值完成 promise 2.3.2.3如果 x 处于拒绝态,用相同的拒绝原因拒绝 promise
" ---首先解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [[Resolve]]
(promise, x),如果 x 有 then 方法且看上去像一个 promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来完成 promise "
可以这么理解该条,如果返回值x
是Promise实例,则该then返回的Promise的状态必须等待x
的状态变化,两者状态同步,你等待我也等待,你fulfilled我也fulfulled,你拒绝我也拒绝。如果promise嵌套再嵌套呢,这就需要一直递归等待x
状态改变。想象这个then返回的new Promise(resolve,reject)里面这两个resolve和reject一直往下递归传,直到最后一个处理,promise的状态与x的状态同步。先看看下面代码,pp2的状态和谁的状态相同呢?
let pp1 = new Promise((resolve, reject) => {
resolve();
})
let pp2 = pp1.then(() => {
return new Promise((resolve, reject) => {
resolve();
}).then(() => {
//A
return new Promise((resolve, reject) => {
resolve();
}).then(() => {
//B
})
})
})
就像这里的代码,嵌套再嵌套,遇到return,根据promsie解析规则,pp2状态先与A所在的then返回的Promise(称为promiseA)状态一致,当运行A时,发现还有返回值,那么PromiseA的状态又和B返回的Pormise状态(PromsieB)一致,因此pp2,PromiseA,PromiseB状态是一致的。相当于一直把pp2的resolve这个函数往下传,实现同步。
有了上面基础,那么分析1.2开头的代码顺序就简单了很多,先打印 ---1 ,---2
没问题,然后遇到了return,那么回调A所在的then返回的Promise(称为PromiseA)状态与谁一致那,联合法则三的PS: !!then里面的回调是在微任务队列运行的,但解析该then函数是直接依次直接解析的!!,那么肯定是最深处的那个then返回的Promise(称为PromiseD),那么显然回调B得等A所在的then返回的Promise (PromiseA)状态改变再执行,然后你得执行完回调D才能改变状态,自然回调B再最后调用,输出
1 2 4 5 6 3
好,看完这里,有没有更深刻?希望多多关注笔者~
2 .实战
2.1 多层嵌套
let pp1 = new Promise((resolve, reject) => {
console.log('---1')
resolve();
})
let pp2 = pp1.then(() => {
console.log('---2');
return new Promise((resolve, reject) => {
console.log('---3');
resolve();
}).then(() => {
console.log('---4');
return new Promise((resolve, reject) => {
resolve();
console.log('---5');
}).then(() => {
console.log('--6');
})
})
}).then(() => {
console.log('--7');
})
2.2 并列的promise
let pp3 = new Promise((resolve, reject) => {
console.log('---1')
resolve();
}).then(() => {
console.log('--2');
}).then(() => {
console.log('--3');
})
let pp4 = new Promise((resolve, reject) => {
console.log('---4')
resolve();
}).then(() => {
console.log('--5');
}).then(() => {
console.log('--6');
})
2.3 公布答案
代码2.1执行顺序为 :
1 2 3 4 5 6 7
代码2.2执行顺序为:
1 4 2 5 3 6
最后,掌握这几种法则,promise轻松拿捏