前言
写此之前,也查阅了很多文章,并结合自己的理解,说说对Event Loop模型的理解、以及对Promise、async/await在任务队列中的影响进行了分析,也给出了多种情形的任务案例以及分析解释,相信大家看完会有所收获;当然,也是自己的理解,难免有所偏差,欢迎大家指正~
一、 宏任务、微任务的基本概念
1.宏任务介绍
宏任务(Macro Task)是指由主线程上的事件触发器(Event Loop)进行调度的任务。宏任务包括但不限于如下几种情况:主线程上的代码块、setTimeout、setInterval、I/O 操作、DOM 事件等。
2.微任务介绍
微任务(Micro Task)是指由其他任务触发的任务。它们的优先级比宏任务更高,会在宏任务队列为空时立即执行。微任务包括但不限于如下几种情况:Promise 的回调函数、MutationObserver 的回调函数等。
3.宏任务、微任务发展及出现的原因:
JavaScript是典型的单线程(自上而下依次执行代码),但是,一个任务耗时过长,或多个任务需要执行时,势必导致线程阻塞,影响视图渲染效果。
在ES3以及以前的版本中,JavaScript本身没有异步任务能力,随着Promise的引入,JavaScript引擎自身也能够发起异步任务了。至此,JavaScript就可分为同步任务及异步任务;而JS又把异步任务做了进一步的划分,分为宏任务与微任务。
由于微任务执行快,一次性可以执行很多个,在当前宏任务执行后立刻清空微任务可以达到伪同步的效果,这对视图渲染效果起到至关重要的作用,这也是区分宏任务、微任务的原因。
4.宏任务、微任务的基本类型
宏任务:
- script(外层同步代码)
- Ajax请求
- setTimeout、setInterval
- postMessage、MessageChannel
- setImmediate(Node.js 环境)、I/O(Node.js 环境)
- ...
微任务
- Promise.then().catch() 和 .finally()
- process.nextTick(Node.js 环境)
- ...
二、 事件循环模型(Event Loop)
如上图,当同步代码执行完毕后,就会执行所有的宏任务,宏任务执行完成后,会判断是否有可执行的微任务;如果有,则执行微任务,完成后,执行宏任务;如果没有,则执行新的宏任务,形成事件循环。
这只是图示宏任务及微任务的执行关系,那么,js在 Event Loop中究竟是如何调用方法去处理的呢?
- 我们写的代码,js会进行任务类型分配,根据类型放入不同的任务队列中;
- Event Loop 会根据执行时机,将需要执行的函数,压入事件调用堆栈中执行;
- 相同的宏任务,也会有不同的执行时机,(类似 setTimeout 的 time),Event Loop 会控制将需要执行的函数压入
总结:Event Loop 在压入事件时,都会判断微任务队列是否还有需要执行的事件:如果有,则优先将需要执行的微任务压入;没有,则依次压入需要执行宏任务!
切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!
切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!
切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!
三、 Promise、async和await 在事件循环中的处理
1.Promise:
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】什么时候执行)。
2. async/await :
简单来说,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 微任务')
这个知识点比较容易忽略,以为 await 回来后,直接继续执行后面的代码,这是不对的!
四、 process.nextTick在事件循环中的处理
process.nextTick是Node环境的变量,process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。所以,nextTick和Promise同时出现时,肯定是process.nextTick() 先执行。
可以类比 Vue.$nextTick(),也是需要执行完这个函数后,才能继续Event Loop。
五、 典型案例
5.1 典型的宏任务、微任务
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');
5.2 宏任务中包含微任务
setTimeout(function () { console.log('1'); new Promise(function (resolve) { console.log('2'); resolve(); }) .then(function () { console.log('3'); }) console.log('4'); }); console.log('5');
5.3 微任务中包含宏任务
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');
5.4 async 宏任务
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()
5.5 node 事件执行分析
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') }) })
5.6 综合案例
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')
六、案例分析
6.1 典型的宏任务、微任务
// 同步任务 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
6.2 宏任务中包含微任务
// 同步任务 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
6.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
6.4 async 宏任务
// 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'); // 最后的宏任务
6.5 node 事件执行分析
// 从上往下: 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
6.6 综合案例
// 这个案例,就应用了 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博客