在 Node 11 版本中,Node 的 Event Loop 已经与 浏览器趋于相同。html
Event Loop也是js老生常谈的一个话题了。2月底看了阮一峰老师的《Node定时器详解》一文后,发现没法彻底对标以前看过的js事件循环执行机制,又查阅了一些其余资料,记为笔记,感受不妥,总结成文。html5
浏览器中与node中事件循环与执行机制不一样,不可混为一谈。 浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现。同时阅读《深刻浅出nodeJs》一书时发现比较当时node机制已有不一样,因此本文node部分针对为此文发布时版本。强烈推荐读下参考连接中的前三篇。node
js执行为单线程(不考虑web worker),全部代码皆在主线程调用栈完成执行。当主线程任务清空后才会去轮询取任务队列中任务。git
异步任务分为task(宏任务,也可称为macroTask)和microtask(微任务)两类。 当知足执行条件时,task和microtask会被放入各自的队列中等待放入主线程执行,咱们把这两个队列称为Task Queue(也叫Macrotask Queue)和Microtask Queue。github
即为同步完成,一个宏任务,全部微任务,一个宏任务,全部微任务......web
new Promise((resolve, reject) =>{console.log(‘同步’);resolve()}).then(() => {console.log('异步')})
,即promise
的then
和catch
才是microtask,自己的内部代码不是。while (true) {
宏任务队列.shift()
微任务队列所有任务()
}
复制代码
js执行为单线程,全部代码皆在主线程调用栈完成执行。当主线程任务清空后才会去轮询取任务队列中任务。api
在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:promise
除上述循环阶段中的任务类型,咱们还剩下浏览器和node共有的microtask和node独有的process.nextTick
,咱们称之为Microtask Queue和NextTick Queue。浏览器
咱们把循环中的几个阶段的执行队列也分别称为Timers Queue、I/O Queue、Check Queue、Close Queue。bash
在进入第一次循环以前,会先进行以下操做:
process.nextTick()
按照咱们的循环的6个阶段依次执行,每次拿出当前阶段中的所有任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,所有6个阶段执行完毕后,进入下轮循环。即:
能够看出,nextTick
优先级比promise
等microtask高。setTimeout
和setInterval
优先级比setImmediate
高。
setImmediate
则会在此轮循环的check阶段执行,若是在timers阶段建立了setTimeout
,因为timers已取出完毕,则会进入下轮循环,check阶段建立timers任务同理。setTimeout
优先级比setImmediate
高,可是因为setTimeout(fn,0)
的真正延迟不可能彻底为0秒,可能出现先建立的setTimeout(fn,0)
而比setImmediate
的回调后执行的状况。while (true) {
loop.forEach((阶段) => {
阶段所有任务()
nextTick所有任务()
microTask所有任务()
})
loop = loop.next
}
复制代码
function sleep(time) {
let startTime = new Date()
while (new Date() - startTime < time) {}
console.log('1s over')
}
setTimeout(() => {
console.log('setTimeout - 1')
setTimeout(() => {
console.log('setTimeout - 1 - 1')
sleep(1000)
})
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 1 - then')
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 1 - then - then')
})
})
sleep(1000)
})
setTimeout(() => {
console.log('setTimeout - 2')
setTimeout(() => {
console.log('setTimeout - 2 - 1')
sleep(1000)
})
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 2 - then')
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 2 - then - then')
})
})
sleep(1000)
})
复制代码
setTimeout - 1 //1为单个task
1s over
setTimeout - 1 - then
setTimeout - 1 - then - then
setTimeout - 2 //2为单个task
1s over
setTimeout - 2 - then
setTimeout - 2 - then - then
setTimeout - 1 - 1
1s over
setTimeout - 2 - 1
1s over
复制代码
setTimeout - 1
1s over
setTimeout - 2 //一、2为单阶段task
1s over
setTimeout - 1 - then
setTimeout - 2 - then
setTimeout - 1 - then - then
setTimeout - 2 - then - then
setTimeout - 1 - 1
1s over
setTimeout - 2 - 1
1s over
复制代码
由此也可看出事件循环在浏览器和node中的不一样。
因为新版 node 执行状况与浏览器相同,因此浏览器环境为例,以 console 输出值代指值所在函数,执行过程以下
<!--执行完主执行线程中的任务。-->
<!--取出Microtask Queue中任务执行直到清空。-->
<!--取出Macrotask Queue中一个任务执行。-->
<!--取出Microtask Queue中任务执行直到清空。-->
<!--重复3和4。-->
以 IQ 代指微任务队列,AQ 代指宏任务队列
1. 执行完主线程中任务:主执行线程执行完毕,setTimeout-一、setTimeout-2 进入等待
2. 清空 IQ:此时 IQ 中无任务
2. 执行 AQ 中一个任务: setTimeout-1 到时间后进入 AQ 中,被执行,执行过程当中 setTimeout-1-1 进入等待状态,setTimeout-1-then 直接进入 IQ 队列,因为 setTimeout-1 中有 1s 等待,此时 setTimeout-2 确定已经进入 AQ,setTimeout-1-1 也随后进入 AQ,此时结束状态为 IQ: [setTimeout-1-then],AQ: [setTimeout-2, setTimeout-1-1]
3. 清空 IQ: 此时 IQ 中有 setTimeout-1-then,执行 setTimeout-1-then,执行过程当中,setTimout-1-then-then 直接被加入 IQ,因此 IQ 没清空,因此继续执行 setTimout-1-then-then,IQ 被清空,此时结束状态为 IQ: [], AQ: [setTimeout-2, setTimeout-1-1]
4. 执行 AQ 中一个任务:即执行 setTimeout-2
5. 清空 IQ: 这一步与 3 类似,因此输出 setTimeout-2-then、setTimeout-2-then-then,IQ 清空,此时结束状态为 IQ: [], AQ: [setTimeout-1-1, setTimeout-2-1]
6. 执行 AQ 中一个任务:即 setTimeout-1-1
7. 清空 IQ: 自己就为空
8. 执行 AQ 中一个任务:即 setTimeout-2-1
复制代码