✊不积跬步,无以至千里;不积小流,无以成江海。
前言
关于任务队列、微任务的定义可以参考之前的笔记
Javascript高级-任务队列与微任务队列 - 掘金 (juejin.cn)
关于promise,可以参考之前的笔记
Javascript高级 - Promise专题 - 掘金 (juejin.cn)
关于async / await,可以参考之前的笔记
Javascript高级-回调与异步 - 掘金 (juejin.cn)
宏任务、微任务、事件执行顺序
宏任务
宏任务是指在当前同步任务完成后执行的任务。宏任务包括:
- setTimeout 和 setInterval 定时器
- DOM 事件
微任务
微任务是指在当前宏任务完成后立即执行的任务。微任务包括:
- Promise.then
执行顺序
在 JavaScript 引擎中,事件循环会不断执行以下步骤:
- 执行所有同步代码
- 执行所有微任务
- 执行所有宏任务
示例讲解promise与微任务
示例1
有代码:
javascript
let p = new Promise(function f1(resolve) {
setTimeout(function f2(){
resolve(2)
console.log(1)
}, 1000)
})
p.then(function f3(v) { console.log(v) })
关于这段代码,运行的详细流程是:
- 运行同步代码f1,创建定时器,初始化p.then
- 1秒之后,f2加入宏队列。此时宏队列:[f2] , 微队列: []。
- 先扫描微队列(为空),再扫描宏队列 (拿出一个任务f2),运行f2。遇到resolve,触发把之前p.then(f3)的f3移入微任务队列(遇到
resolve
表示异步状态可以被激活,事件可以进入队列中来。由于resolve
是在执行阶段执行的,因此f3
函数会被加入到微任务队列中。) - 之后执行f2函数的最后一步,输出1。此时宏队列:[] , 微队列: [f3]。
- 扫描微队列,依次拿出并运行全部任务,执行f3输出2。
所以最输出结果是1秒之后立即依次出现 1、2
核心思路:
- promise中有些是同步执行的,比如说resolve之前的代码;有些是异步执行的,比如说then,setTimeout等。
- 在 Promise 中,先扫描微任务,再扫描宏任务。
- 当
resolve
被调用时,Promise 会将其值传递给.then
方法中指定的函数。
示例2
有代码:
javascript
let p1 = new Promise(function f1(resolve1) {
setTimeout(resolve1)
})
let p2 = p1.then(function f2(v) { console.log(2) })
let p3 = p2.then(function f3(v) { console.log(3) })
let p11 = new Promise(function f11(resolve2) {
setTimeout(resolve2)
})
let p22 = p11.then(function f22(v) { console.log(22) })
let p33 = p22.then(function f33(v) { console.log(33) })
以上代码的执行流程是:
- 先执行同步的代码。运行f1,立即得到一个pending状态的Promise对象p1(f1里面的resolve1函数被加入宏任务,还没开始执行)。运行p1.then得到一个pending状态的Promise对象p2。... ,同理得到一个pending状态的Promise对象p33。此时宏队列:[resolve1, resolve2] , 微队列: []。
- 因微队列目前为空,所以扫描宏队列,拿出resolve1运行,导致p1被resolve(从pending变成fulfilled),从而导致p1.then(f2)中的f2被加入微队列。此时宏队列:[resolve2] ,微队列:[f2]。
- 扫描全部微任务,拿出f2运行,输出2 。f2运行结束(函数结束或者遇到return)时,触发p2内部状态的变化(p2从pending变成fulfilled),导致p2.then(f3)中的f3加入微任务队列。此时宏队列:[resolve2] ,微队列:[f3] 。微任务队列不为空,拿出f3,运行输出3 ,此时p3变成fulfilled状态。宏队列:[resolve2] ,微队列:[]。
- 扫描下一个宏任务,拿出resolve2,运行,导致p11被resolve,从而导致p11.then(f22)中的f22被加入微队列。此时宏队列:[] ,微队列:[f22]。
- 扫描全部微任务,拿出f22运行,输出22 。f22运行结束时,触发p22从pending变成fulfilled,导致p22.then(f33)中的f33加入微任务队列。此时宏队列:[] ,微队列:[f33] 。微任务队列还未扫描完,拿出f33,运行输出33 ,此时p33变成fulfilled状态。微队列:[]。
最终输出顺序为 2、3、22、33。
核心思想:
- 在 Promise 对象刚创建时,其状态默认是 pending。
- 调用
resolve
方法时,Promise 对象会从 pending 变为 fulfilled。 - Promise 对象所代表的操作成功完成时,Promise 对象会从 pending 变为 fulfilled。
- Promise 对象的状态一旦变为 fulfilled,就不会再改变。
- 如果 Promise 对象所代表的操作失败,则 Promise 对象的状态会变为 rejected。
示例1 & 2总结
关于上面两个例子,总结的解题思路:
- promise对象创建时,状态总是默认为pending,但一旦状态转变为resolve(reject),状态就会变为fulfilled(rejected)。一旦状态改变,.then中的事件立刻加入微队列。
- 总是优先执行微队列中的任务,之后再考虑宏队列中的任务。
对于Promise我们需要知道,链式调用.then之后会返回一个新的Promise对象。
因此示例2中的示例有时也写做:
javascript
new Promise( resolve => setTimeout(resolve) )
.then( v => console.log(2) )
.then( v => console.log(3) )
new Promise( resolve => setTimeout(resolve) )
.then( v => console.log(22) )
.then( v => console.log(33) )
示例三
对于上面提到的两点,再用一个例子:
javascript
let p1 = Promise.resolve(1)
let p2 = p1.then(function f2() {
console.log(2)
})
let p3 = p2.then(function f3() {
console.log(3)
})
let p11 = new Promise(function f11(resolve) {
resolve(11)
})
let p22 = p11.then(function f22() {
console.log(22)
})
let p33 = p22.then(function f33() {
console.log(33)
})
以上代码的执行流程是:
- 先执行同步的代码。运行resolve1,立即得到一个fulfilled状态的Promise对象p1。p11是立即执行的同步函数,运行resolve11,立即得到一个fulfilled状态的Promise对象p11
- 因为resolve已经执行,所以立刻执行p1then & p11 then;p2是p1的then,p22是p11的then,所以f2 & f22放到微任务队列中。目前宏队列:[] ;微队列: [f2,f22]。
- p3是p2的then,暂时还不会运行;p33是p22的then,暂时还不会运行。
- 之后立刻执行微队列,打印2 & 22。
- 由于p2 / p22状态从pending变为fulfilled,所以p3 & p33的then被激活,进入到微队列。目前宏队列:[] ;微队列: [f3,f33]。
- 之后立刻执行微队列,打印3 & 33。
最后输出结果为 2,22,3,33。以上代码等价于常见链式写法:
javascript
Promise.resolve(1)
.then(() => console.log(2))
.then(() => console.log(3))
new Promise(resolve => resolve())
.then(() => console.log(22))
.then(() => console.log(33))
示例四
这是一个关于Promise 微任务的题目:
javascript
let p1 = Promise.resolve()
.then(function f1(v) { console.log(1) })
.then(function f2(v) { console.log(2) })
.then(function f3(v) { console.log(3) })
p1.then(function f4(v) { console.log(4) })
p1.then(function f5(v) { console.log(5) })
let p2 = Promise.resolve()
.then(function f11(v) { console.log(11) })
.then(function f22(v) { console.log(22) })
.then(function f33(v) { console.log(33) })
p2.then(function f44(v) { console.log(44) })
p2.then(function f55(v) { console.log(55) })
以上代码的执行流程是:
- 先执行同步的代码。运行resolve,立即得到一个fulfilled状态的Promise对象p1 & p2。
- 但是p1 & p2必须运行完全部的resolve后的then,才能够被算做是p1完全被resolve。
- 所以按照顺序,先将f1 & f11放入微任务队列。目前宏队列:[] ;微队列: [f1,f11]。
- 之后优先执行微任务队列,即输出 1 & 11。之后微队列中的f1 & f11状态变为fulfilled被释放后,f2 & f22由于是f1 & f11的then所以马上被推入微队列。
- 同理f3 & f33。因此在p1 & p11彻底被resolve时,输出为 1,11,2,22,3,33。
- 当p1 & p2被完全resolve后,p1 / p2 的then被推入微队列。由于优先执行p1的then(代码顺序问题)。导致,宏队列:[] ;微队列: [f4,f5,f44,f55]。
- 所以输出为 4,5,44,55。
因此最后的输出为,1,11,2,22,3,33,4,5,44,55。
示例讲解async/await转换Promise,以及微任务
await
操作符用于等待一个Promise对象。如果该值不是一个 Promise,await 会把该值转换为 resolved 的Promise。
示例一
有一段代码:
javascript
async function async1(){
console.log(1)
await 1
console.log(2)
}
let p = async1()
console.log(p)
async函数一定返回Promise对象,上面的代码等价于code 2:
javascript
function async1() {
console.log(1)
return new Promise(resolve => {
resolve(1)
}).then(() => {
console.log(2)
})
}
let p = async1()
console.log(p)
已知await = promise对象,所以可以将await语句替换为,return new promise().then() 且 resolve(1)。
因此第一步先打印1,之后返回一个promise对象,当这个对象fulfilled之后,才继续将then加入到微队列中打印2。
所以打印出来的值是1,promise{ < pending > },2。
示例二
有一段代码:
javascript
async function async2() {
console.log(2)
return 2
}
async function async1(){
console.log(1)
await async2()
console.log(3)
}
async1()
由于await 语句之后的代码是await的等价的Proimise对象的then逻辑,因此代码可被改写为:
scss
function async2() {
console.log(2)
return Promise.resolve(2)
}
function async1() {
console.log(1)
return async2()
.then(() => console.log(3))
}
async1()
示例三
又有一段代码:
javascript
async function async2() {
console.log('async2 start')
return new Promise((resolve, reject) => {
resolve()
console.log('async2 promise')
})
}
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async1()
如果async中又有return promise并且被await,改写方法如下:
javascript
function async2() {
console.log('async2 start')
return new Promise(resolve2 => {
new Promise((resolve, reject) => {
resolve()
console.log('async2 promise')
}).then(() => resolve2())
})
}
- 先假设async2中的return new Promise(...)替换为return 2
- 所以await async2()在async2中就会被改写为 return Promise.resolve(2)【示例二】
- 这时再把 2 和 new Promise(...)替换回来变为 return Promise.resolve(new Promise(...))
- 但由于示例2中的.resolve(2)这种写法不适用于这个例子,要把这个例子再标准化一下,变为return new Promise( () => { new Promise(...) }).then()
- 由于替换为resolve的promise,不存在其他情况【通常为(resolve, reject)】,所以标准化后的promise传入的参数为resolve,为区分变量名,命名为resolve2,即改写为return new Promise( resolve2 => { new Promise(...) }).then()
- then中使用箭头函数执行resolve2(),即改写为return new Promise( resolve2 => { new Promise(...) }).then( () => resolve2() )
总结示例一二三
- await等价于返回一个promise对象,后续的操作用.then来表示
- 如果await转换为promise对象时,对应函数中有返回值,则返回值改写为return一个resolve的promise对象
- 如果await转换为promise对象时,对应函数中有返回一个promise对象,则返回值也会改写为return一个resolve的promise对象,但格式应转换为new promise().then()的各式。
示例四
有代码:
javascript
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 start');
return new Promise((resolve, reject) => {
resolve();
console.log('async2 promise');
})
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
}).then(function() {
console.log('promise3');
});
console.log('script end');
则可根据总结转换为:
javascript
function async1() {
console.log('async1 start')
return new Promise(resolve => { // async 函数返回一个Promise对象,由async2()得到的Promise对象的resolve来触发自己的resovle
async2().then(v => resolve(v))
}).then(()=> {
console.log('async1 end')
})
}
function async2() {
console.log('async2 start')
return new Promise(resolve2 => { // 返回一个新的Promise对象,由原来async函数里return的Promise对象的resovle来触发自己的resolve
new Promise((resolve, reject) => {
resolve()
console.log('async2 promise')
}).then(() => resolve2())
})
}
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
async1()
new Promise(function(resolve) {
console.log('promise1')
resolve()
}).then(function() {
console.log('promise2')
}).then(function() {
console.log('promise3')
});
console.log('script end')
对于运行步骤:
- 首先输出'script start',之后向下运行
- 执行第22行setTimeout,因此将这句话放入宏队列中,宏队列:[22行] ,微队列:[]
- 之后进入到async1中,第一句为同步代码,因此输出'async1 start'
- 由于第3行为同步代码,因此执行接下来的函数,进入到async2
- 进入到async2中,第一句为同步代码,因此输出'async2 start'
- 由于第12行为同步代码,因此进入到async2的new promise
- 遇到resolve,因此把第16行中then的事件加入到微队列中,此时,宏队列:[22行] ,微队列:[16行]
- 之后继续向下执行第15行,输出'async2 promise'。async2原本的promise结束。
- 到这里,两个函数中的立即执行的promise就已经完成了,但是实际上代码才走到第24行,还要继续推进第25行的promise
- 进入到26行中,第一句为同步代码,因此输出'promise1'。
- 由于27行遇见了resolve,所以28行中then的func被激活,因此,宏队列:[22行] ,微队列:[16行,29行]
- 25-32行的promise函数立即执行的部分已经结束,那么则开始执行第33行'script end'。
- 至此,所有的同步代码已经结束。下面开始运行微队列中的代码。
- 首先运行第16行,执行resolve2(),导致整个async2被solve,因此第4行中的then加入了任务队列,宏队列:[22行] ,微队列:[29行,4行]。
- 之后运行第29行,输出'promise2 ',导致第30行加入了任务队列,宏队列:[22行] ,微队列:[4行,31行]。
- 之后运行第4行,第4行结束后,导致async自己的promise状态结束,它的then进入队列,即宏队列:[22行] ,微队列:[31行,第6行]。
- 之后运行第31行,输出'promise3',25-32行的事件结束。
- 之后运行第6行,输出'async1 end'。微队列所有任务结束。
- 结束后运行宏队列任务,输出'setTimeout'。所有任务结束。
因此结果为:
sql
script start
async1 start
async2 start
async2 promise
promise1
script end
promise2
promise3
async1 end
setTimeout