目录
三、 Promise、async和await 在事件循环中的处理
这个知识点比较容易忽略,以为 await 回来后,直接继续执行后面的代码,这是不对的!
写此之前,也查阅了很多文章,并结合自己的理解,说说对Event Loop模型的理解、以及对Promise、async/await在任务队列中的影响进行了分析,也给出了多种情形的任务案例以及分析解释,相信大家看完会有所收获;当然,也是自己的理解,难免有所偏差,欢迎大家指正~
宏任务(Macro Task)是指由主线程上的事件触发器(Event Loop)进行调度的任务。宏任务包括但不限于如下几种情况:主线程上的代码块、setTimeout、setInterval、I/O 操作、DOM 事件等。
微任务(Micro Task)是指由其他任务触发的任务。它们的优先级比宏任务更高,会在宏任务队列为空时立即执行。微任务包括但不限于如下几种情况:Promise 的回调函数、MutationObserver 的回调函数等。
JavaScript是典型的单线程(自上而下依次执行代码),但是,一个任务耗时过长,或多个任务需要执行时,势必导致线程阻塞,影响视图渲染效果。
在ES3以及以前的版本中,JavaScript本身没有异步任务能力,随着Promise的引入,JavaScript引擎自身也能够发起异步任务了。至此,JavaScript就可分为同步任务及异步任务;而JS又把异步任务做了进一步的划分,分为宏任务与微任务。
由于微任务执行快,一次性可以执行很多个,在当前宏任务执行后立刻清空微任务可以达到伪同步的效果,这对视图渲染效果起到至关重要的作用,这也是区分宏任务、微任务的原因。
宏任务:
script(外层同步代码)
setTimeout、setInterval
postMessage、MessageChannel
setImmediate(Node.js 环境)、I/O(Node.js 环境)
...
微任务
Promise.then().catch() 和 .finally()
process.nextTick(Node.js 环境)
...
如上图,当同步代码执行完毕后,就会执行所有的宏任务,宏任务执行完成后,会判断是否有可执行的微任务;如果有,则执行微任务,完成后,执行宏任务;如果没有,则执行新的宏任务,形成事件循环。
这只是图示宏任务及微任务的执行关系,那么,js在 Event Loop中究竟是如何调用方法去处理的呢?
总结:Event Loop 在压入事件时,都会判断微任务队列是否还有需要执行的事件:如果有,则优先将需要执行的微任务压入;没有,则依次压入需要执行宏任务!
切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!
切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!
切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!
new Promise 创建实例的过程是同步的哦!
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
})
console.log(3);
// 结果: 1 2 3
但是,Promise.then().catch().finally()中的回调,是微任务。
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve(); // 触发 then 回调
// reject(); // 触发 catch 回调
}).then(()=>{
console.log('then')
}).catch(()=>{
console.log('catch')
}).finally(()=>{
console.log('finally')
})
console.log(3);
// 结果: 1 2 3 then finally
上图是菜鸟教程-JavaScript Promise 中对Promise的讲解,我们想一下为啥要这样设计:
我们假设Promise 不是立即执行的,会有什么后果?利用Promise,多是封装异步请求(Ajax),而请求不是立即请求的,还需要等待Promise 任务执行,那么我们就失去了网络请求的时效性,会导致页面等待渲染(因为我们上面提及的 Event Loop 会根据事件执行时机,选择将事件压入堆栈中,我们无法保证自己写的【不是立即执行Promise】什么时候执行)。
简单来说,async是通过Promise包装异步任务。
async function fun1() {
console.log('fun1 start')
await fun2(); // 等待 fun2 函数执行完成
console.log('fun1 end')
}
async function fun2() {
console.log('fun2 start')
console.log('fun2 end')
}
fun1()
// 输出结果: fun1 start、fun2 start、fun2 end、fun1 end
遇到 await 则需要等待 await 后的代码执行完成后,在往下执行代码。因此,可以将 await 看作抢夺线程的标记,fun1 中,本来是同步执行的,但是 await 的出现,导致线程执行了 fun2 的代码后,再次回到await,往后执行。
同时,await后面的代码,会进入then微任务中!!!
同时,await后面的代码,会进入then微任务中!!!
同时,await后面的代码,会进入then微任务中!!!
async function fun1() {
console.log('fun1 start')
await fun2(); // 等待 fun2 函数执行完成
console.log('我是 await 后面的代码')
console.log('fun1 end')
}
async function fun2() {
console.log('fun2 start')
console.log('fun2 end')
}
fun1()
console.log('await 阻塞,导致 await后面代码进入 then 微任务')
process.nextTick是Node环境的变量,process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。所以,nextTick和Promise同时出现时,肯定是process.nextTick() 先执行。
可以类比 Vue.$nextTick(),也是需要执行完这个函数后,才能继续Event Loop。
setTimeout(function () {
console.log('1');
});
new Promise(function (resolve) {
console.log('2');
resolve();
})
.then(function () {
console.log('3');
})
.then(function () {
console.log('4')
});
console.log('5');
setTimeout(function () {
console.log('1');
new Promise(function (resolve) {
console.log('2');
resolve();
})
.then(function () {
console.log('3');
})
console.log('4');
});
console.log('5');
console.log('1');
new Promise(function (resolve) {
console.log('2');
resolve();
})
.then(function () {
console.log('3');
setTimeout(function () {
console.log('4');
})
console.log('5');
})
console.log('6');
async function fun1() {
console.log('fun1 start')
setTimeout(function () {
console.log('fun1 setTimeout');
});
await fun2();
console.log('fun1 end')
}
async function fun2() {
console.log('fun2 start')
new Promise((resolve)=>{
console.log('fun2 Promise')
resolve()
})
.then(()=>{
console.log('fun2 Promise then')
})
console.log('fun2 end')
}
fun1()
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
console.log('script start')
async function fun1() {
console.log('fun1 start')
process.nextTick(function() {
console.log('fun1 process nextTick');
})
setTimeout(function () {
console.log('fun1 setTimeout');
new Promise(function (resolve) {
console.log('fun1 Promise');
resolve();
})
.then(function () {
console.log('fun1 Promise then');
setTimeout(function () {
console.log('fun1 Promise then setTimeout');
})
console.log('fun1 Promise then end');
})
});
await fun2();
console.log('fun1 end')
}
async function fun2() {
console.log('fun2 start')
setTimeout(function () {
console.log('fun2 setTimeout');
});
new Promise((resolve)=>{
console.log('fun2 Promise')
resolve()
})
.then(()=>{
console.log('fun2 Promise then')
})
console.log('fun2 end')
}
fun1()
setTimeout(function() {
console.log('setTimeout-000')
}, 0)
new Promise(resolve => {
console.log('Promise')
process.nextTick(function() {
console.log('Promise process nextTick');
})
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
process.nextTick(function() {
console.log('promise2 process nextTick');
})
})
console.log('script end')
// 同步任务
console.log('2'); // new promise 实例化过程
console.log('5');
//
// 将 setTimeout console.log('1'); 放入宏任务队列
// 将Promise 的回调放入微任务队列
// then console.log('3');
// then console.log('4')
// 先微任务
console.log('3');
console.log('4')
// 再宏任务
console.log('1');
// 因此,输出结果: 2 5 3 4 1
// 同步任务
console.log('5');
// 将 setTimeout 放入宏任务队列,此时,没有微任务执行,因此,开始执行setTImeout宏任务
console.log('1');
// new Promise 实例化 同步执行:
console.log('2');
// 将Promise.then 回调放入微任务
// 当前(setTimeout)的宏任务事件还没有执行完!!!
// 注意哈!!当前(setTimeout)的宏任务事件还没有执行完!!!,事件未跳出当前 Loop
console.log('4');
// 执行完宏任务,开始执行 微任务
console.log('3');
// 因此,结果为: 5 1 2 4 3
// 同步代码:
console.log('1');
// new Promise
console.log('2');
console.log('6');
// 微任务:Promise.then
console.log('3');
console.log('5');
// 结束当前 Loop[有上个例子,这个就不难理解了]
// 开启宏任务
console.log('4');
// 因此,结果为:1 2 6 3 5 4
// fun1
console.log('fun1 start')
// 将setTimeout 放入宏任务队列
// await fun2(); 进入 fun2
console.log('fun2 start')
// new Promise
console.log('fun2 Promise')
// 将 then 放入微任务
console.log('fun2 end') // 当前任务队列
// 有微任务,先执行微任务
console.log('fun2 Promise then')
// 回到 await 处
console.log('fun1 end') // 当前 fun1 队列
console.log('fun1 setTimeout'); // 最后的宏任务
// 从上往下:
console.log('1'); // 同步代码
// setTimeout 宏任务1
// process.nextTick 微任务1
console.log('7'); // new Promise
// Promise.then 微任务2
// setTimeout 宏任务2
// -- 开始执行微任务
console.log('6');
console.log('8')
// -- 开始宏任务1
console.log('2');
// process.nextTick 微任务!!!
console.log('4'); // new Promise
// Promise.then 微任务!!!
// 到此,当前宏任务已执行完毕,有微任务,需要先执行微任务
console.log('3');
console.log('5')
// 执行宏任务 2
console.log('9');
// process.nextTick 微任务
console.log('11');// new Promise
// Promise.then 微任务!!!
console.log('10');
console.log('12')
// 因此,结果为:1 7 6 8 2 4 3 5 9 11 10 12
// 这个案例,就应用了 await 导致的 then 微任务细节,我第一次分析也错了
// 还涉及了process.nextTick node 的执行时机优先
// 开始分析:
console.log('script start')
// fun1() 进入 fun1
console.log('fun1 start')
// 生成微任务 process.nextTick(fun1)
// 生成 宏任务 setTimeout (fun1)
await fun2(); // 进入 fun2
console.log('fun2 start')
// 生成宏任务 setTimeout (fun2)
console.log('fun2 Promise') // new Promise
// 生成 Promise.then 微任务
console.log('fun2 end')
// !!!此时,fun2 已经有返回值了,不需要等待 fun2 中的事件执行,回到 await 处,被标记了 await .then 的微任务
// 因此,执行主任务
console.log('Promise') // new Promise
// 生成 Promise process.nextTick 微任务
// 生成 Promise1.then 微任务
// 生成 Promise2.then 微任务
console.log('script end')
/**
* 分析微任务队列
* 1. 第一个微任务:process.nextTick(fun1)
* 2. fun2 Promise.then // 容易漏
* 3. await .then 的微任务 !!!!!!!!!!!!!
* 4. Promise process.nextTick 微任务
* 5. Promise1.then 微任务
* 6. Promise2.then 微任务
*
*/
// 根据 Node process 优先级,先执行 process
console.log('fun1 process nextTick');
console.log('Promise process nextTick');
console.log('fun2 Promise then')
// await.then微任务[await 后的所有代码,如果还有任务,具体再分析即可]
console.log('fun1 end')
console.log('promise1')
console.log('promise2') // 执行到这,又生成新的 process.nextTick 微任务,又先执行
console.log('promise2 process nextTick');
// 没有微任务了,开始执行宏任务
console.log('fun1 setTimeout');
console.log('fun1 Promise'); // 生成新的 promise.then 微任务,当前宏任务已执行完成,开始执行微任务
console.log('fun1 Promise then'); // 生成新的 宏任务 fun1 Promise then setTimeout
console.log('fun1 Promise then end');
/**
* 此时,分析宏任务队列
* 1. 第一个 是 fun2 setTimeout
* 2. setTimeout(function () { console.log('setTimeout-000') }, 0)
* 3. fun1 Promise then setTimeout
* */
// 因此, 依次执行宏任务
console.log('fun2 setTimeout');
console.log('setTimeout-000')
console.log('fun1 Promise then setTimeout');
这个案例比较复杂,某些事件容易漏掉,因此,建议大家手动勾起来,每一个事件都对应到事件队列中,这个案例,考察两个点,一个是 await的处理及 node.process 的优先级。大家弄懂这个案例,出去面试,手撕这种题目应该不是问题了。
在实际开发中,我们常常会利用宏任务与微任务的执行顺序来进行任务的调度。通过将一些耗时较长的任务放在宏任务中,可以保证其他任务的及时执行;而将一些需要优先执行的任务放在微任务中,可以保证其优先级更高。
需要注意的是,宏任务与微任务的执行顺序是由浏览器的事件循环机制控制的,不同的浏览器可能存在一些差异。因此,在实际开发中,我们应该合理地安排宏任务与微任务的使用,避免出现一些不可预期的结果。
参考文章:
宏任务和微任务的执行顺序_微任务和宏任务的执行顺序-CSDN博客
宏任务与微任务执行顺序(超详细讲解)_宏任务和微任务谁先执行-CSDN博客