javascript的宏任务与微任务,EventLoop(转载)
比较详细的介绍
javascript的宏任务与微任务,EventLoop(转载)
比较详细的介绍
EventLoop是一个执行模型,在不同的有不同的实现,浏览器和NodeJS基于不同的技术实现了各自的EventLoop。
JavaScript是单线程脚本语言。所以,在一行代码的执行过程过,必然不会执行另一行代码的,就行你在使用了alert(1)
以后在后面疯狂的console.log()
,如果执行到 alert(1)
,你没有关闭这个弹窗,后面的console.log()
是永远都不会执行的,因为 alert()
这个任务还没有执行完成,下面的代码没法执行。通俗一点就是:如果你去食堂打饭,前面排了很长的队,如果你想要打到饭,那么你需要等前面的小可爱都能够顺利的打完饭才可以,你是不能够插队的。那什么是宏任务,什么又是微任务呢?
同样是打饭的例子,你要打饭这件事请就是宏任务。这是一个大的事件。当轮到你打饭的时候,事件执行到你这里了,这个时候阿姨开始给你打饭,后面的同学还在等待着。但是你去打饭不单单的就是打饭,你会询问每种菜是什么,价格是多少,有没有XXX菜,有没有汤一样,那这些询问可以比作是微任务。当你的宏任务与微任务都执行完成了,相当于你的这一轮时间执行完成,这个时候开始执行下一轮事件,也就是下一个同学开始打饭了。同样的,下面的一轮循环中也可能存在微任务。
通过上面的例子,如果能有大概的明白了什么是宏任务,什么是微任务了。
macrotask,也叫 tasks,主要的工作如下
一些异步任务的回调会以此进入 macrotask queue(宏任务队列)
,等等后续被调用,这些异步函数包括:
microtask,也叫 jobs,注意的工作如下
另一些异步回调会进入 microtask queue(微任务队列)
,等待后续被调用,这些异步函数包括:
这里有一点需要注意的:
Promise.then()
与new Promise(() => {}).then()
是不同的,前面的是一个微任务,后面的new Promise()
这一部分是一个构造函数,这是一个同步任务,后面的.then()
才是一个微任务,这一点是非常重要的。
关于宏任务与微任务我们看看下面的执行流程
最开始有一个执行栈,当执行到带有异步操作的宏任务的时候,比如 setTimeout 的时候就会将这个异步任务存在背景线程里面,待本次的事件执行完成以后再去执行微任务。即图中 Stack --> Background Thread
。但是需要注意到,从 Stack --> Microtask Queue
还有一条路线,意思就是在当前这轮的任务中还有执行微任务的操作。当前轮的微任务优先于宏任务异步操作先执行,执行完成到 loop
中,进入到下一轮。下一轮执行之前的宏任务的异步操作,比如 setTimeout
。此时,如果这个异步任务中还有微任务,那么就会执行完成这个微任务,在执行下一个异步任务。就这样一次的循环。
回到最开始的那道题上面
setTimeout( () => console.log(4)) new Promise(resolve => { resolve() console.log(1) }).then( () => { console.log(3) }) Promise.resolve(5).then(() => console.log(5)) console.log(2)
整个这一串代码我们所在的层级我们看做一个任务,其中我们先执行同步代码。第一行的 setTimeout
是异步代码,跳过,来到了 new Promise(...)
这一段代码。前面提到过,这种方式是一个构造函数,是一个同步代码,所以执行同步代码里面的函数,即 console.log(1)
,接下来是一个 then
的异步,跳过。在往下,是一个Promise.then()
的异步,跳过。最后一个是一段同步代码 console.log(2)
。所以,这一轮中我们知道打印了1, 2
两个值。接下来进入下一步,即之前我们跳过的异步的代码。从上午下,第一个是 setTimeout
,还有两个是 Promise.then()
。setTimeout
是宏任务的异步,Promise.then()
是微任务的异步,微任务是优先于宏任务执行的,所以,此时会先跳过 setTimeout
任务,执行两个 Promise.then()
的微任务。所以此时会执行 console.log(3)
和 console.log(5)
两个函数。最后就只剩下 setTimeout
函数没有执行,所以最后执行 console.log(4)
。
综上:最后的执行结果是 1, 2, 3, 5, 4
。
这只是我们的推测的结果,我们来看看在浏览器中的实际的打印结果是什么?
从图中可以看到,实际的运行结果与我们推测的结果是一一致的。所以,我们上面的分析步骤是正确的。
但是有一个问题,什么呢?可以看到,在浏览器中,会有一个 undefined
的返回值。为什么呢?这是因为浏览器将上面的一整段代码当成一个函数,而这个函数执行完成以后返回了 undefined
。那么?这就完了吗?没有。我们看看浏览器返回的截图中,3,5
两个数字其实是在 undefined
前面。3,5
两个数是两个 Promise.then()
中的 console.log()
的打印值,而 undefined
在这里可以作为一轮任务的结束。这表明的意思就是,微任务会在下一轮任务开始前执行。
这一切都是针对于浏览器的EventLoop。在NodeJS的环境中,可能就会有不同的结果。至于结果如何,我们暂时先不讨论,在来看一段代码。
setTimeout( () => { new Promise(resolve => { resolve() console.log(4) }).then(() => { console.log(7) }) }) new Promise(resolve => { resolve() console.log(1) }).then( () => { console.log(3) }) setTimeout( () => { Promise.resolve(6).then(() => console.log(6)) new Promise(resolve => { resolve() console.log(8) }).then(() => { console.log(9) }) }) Promise.resolve(5).then(() => console.log(5)) console.log(2)
在浏览器中执行结果:点击查看
var eventloopBtn = document.getElementById("evnetloop-btn"); var eventloopResult = document.getElementById("evnetloop-result"); eventloopBtn.addEventListener("click", () => { eventloopResult.innerHTML = "1,2,3,5,4,7,8,6,9" })
上面就是关于在浏览器中的EventLoop。附上浏览器上面的可视化操作
虽然NodeJS中的JavaScript运行环境也是V8,也是单线程,但是,还是有一些与浏览器中的表现是不一样的。
上面的图片的上半部分来自NodeJS官网。下面的图片来自互联网。
同样的两段代码,我们在node环境中执行一下,看看结果。
从上面的图中可以看到,实际的运行结果与浏览器中的运行结果并无二致。
在来看看另一段代码
setTimeout( () => { new Promise(resolve => { resolve() console.log(4) }).then(() => { console.log(7) }) }) new Promise(resolve => { resolve() console.log(1) }).then( () => { console.log(3) }) setTimeout( () => { Promise.resolve(6).then(() => console.log(6)) new Promise(resolve => { resolve() console.log(8) }).then(() => { console.log(9) }) }) Promise.resolve(5).then(() => console.log(5)) console.log(2)
他的执行结果是:1,2,3,5,4,8,7,6,9
。
与浏览器的1,2,3,5,4,7,8,6,9
不同。
在大部分情况下,浏览器与NodeJS的运行没有区别,唯一有区别的是在第二轮事件执行的时候,如果有多个宏任务(setTimeout
),浏览器会依次的执行宏任务,上一个宏任务执行完成了在执行下一个宏任务。在NodeJS中,则是相当于并行执行,相当于把所有的宏任务组合到一个宏任务中,再在这个组合后宏任务中,依次执行同步代码 --> 微任务 --> 宏任务
。
关于 process.nextTick
,就只需要记住一点,那就是 process.nextTick
优先于其他的微任务执行。
所以,下面的代码中:
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') }) })
分析(以Node作为运行环境,因为process在node中才存在):
第一轮事件循环流程:
console.log(1)
,输出 1
setTimeout1
process.nextTick
,其回调函数被分发到微任务的 Event Queue 中,等待执行。console.log(7)
,输出 7
。接着Promise.then()函数被分发到微任务的 Event Queue 中,等待执行。
遇到setTimeout,其回调函数被分发到宏任务的 Event Queue 中,等待执行。这里标记为setTimeout2
将上面的统计一下
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | process.nextTick |
setTimeout2 | Promise.then() |
第一轮事件循环同步代码执行完成,接下来执行微任务
。
微任务有两个,一个是 process.nextTick
,里一个是 Promise.then()
。
前面说了,process.nextTick
优先于其他的微任务执行,所以
6
8
到此,第一轮事件循环结束,最终第一轮事件的输出为 1,7,6,8
。开始执行第二轮事件循环(setTimeout)。
第二轮事件循环分析
setTimeout1
与 setTimeout2
中先找同步代码2
process_1
4
, Promise.then() 放到微任务的Event Queue中,等待执行。这里标记为Promise_1
9
process_2
11
, Promise.then() 放到微任务的Event Queue中,等待执行。这里标记为Promise_2
第二轮的统计
第二轮宏任务Event Queue | 第二轮微任务Event Queue |
---|---|
process_1 | |
Promise_1 | |
process_2 | |
Promise_2 |
第二轮没有事件循环中没有宏任务,有四个微任务。
四个微任务中,有两个 process
process_1
和 process_2
。输出:3, 10
Promise_1
和 Promise_2
。输出:5, 12
所以第二轮输出:2,4,9,11,3,10,5,12
最终的输出为:1,7,6,8,2,4,9,11,3,10,5,12
。
如果是在浏览器中,排除掉process
的输出,结果为:1,7,8,2,4,5,9,11,12
在官方文档中的定义,setImmediate 为一次Event Loop执行完毕后调用。setTimeout 则是通过计算一个延迟时间后进行执行。
但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。因为如果主进程中先注册了两个任务,然后执行的代码耗时超过XXs,而这时定时器已经处于可执行回调的状态了。所以会先执行定时器,而执行完定时器以后才是结束了一次Event Loop,这时才会执行setImmediate。
setTimeout(() => console.log('setTimeout')) setImmediate(() => console.log('setImmediate'))
node环境下执行上面的代码,可以看到如下结果
这两个console的结果是随机的。
我们可以通过一些处理,使得我们可以先执行 setTimeout
或者是 setImmediate
。
但是如果后续添加一些代码以后,就可以保证setTimeout一定会在setImmediate之前触发了:
setTimeout(_ => console.log('setTimeout')) setImmediate(_ => console.log('setImmediate')) let countdown = 1e9 while(countdonn--) { } // 我们确保这个循环的执行速度会超过定时器的倒计时,导致这轮循环没有结束时,setTimeout已经可以执行回调了,所以会先执行`setTimeout`再结束这一轮循环,也就是说开始执行`setImmediate`
如果在另一个宏任务中,必然是setImmediate先执行:
require('fs').readFile(__dirname, _ => { setTimeout(_ => console.log('timeout')) setImmediate(_ => console.log('immediate')) }) // 如果使用一个设置了延迟的setTimeout也可以实现相同的效果
上面的为什么有这样的解决方法,从上面的定义中就可以看出来。
因为,async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then效果类似
setTimeout(() => console.log(4)) async function main() { console.log(1) await Promise.resolve() console.log(3) } main() console.log(2)
输出的结果是:1,2,3,4
。
可以理解为,await
以前的代码,相当于与 new Promise
的同构代码,以后的代码相当于 Promise.then
。
之前了解过JavaScript单线程,也了解过JavaScript代码的执行顺序,但是宏任务与微任务也是最近才听说的,这对于一个从事两年前端的开发者真的是,我自己的过失。或需又是因为我是转行的,没有过相关的基础,没有接触到这方面的只是。不过现在我很高兴,因为我对JavaScript的执行有了更多的了解,相比于之前的只是,真的是了解了很多。学习永远都不晚,就怕你从来都不想去了解。在了解EventLoop,宏任务与微任务,JavaScript单线程的时候,参考了一些文档
转载:https://cloud.tencent.com/developer/article/1476737(不过原创作者demo的第一个示例执行是有问题的,所以本文暂未贴出)
您的鼓励是我前进的动力---
使用微信扫描二维码完成支付