javascript node 事件循环相关执行顺序
最近笔试某小游戏厂的时候遇见 node event loop 相关的题. 烟雨仔一直认为自己对 event loop 已经很了解了, 但还是入了坑.
在阅读这篇文章之前, 如果你是想了解 浏览器 或 node event loop 更加底层的内容, 那么请移步至 node 官方. 本篇只适合想要对 node.js event loop 相关执行顺序略有了解的新手.
1. 浏览器 event loop
以下代码在众多的面试笔试题中, 是挺常见的. 在 chrome 浏览器中按序应该打印:
start ;
async1 start;
async2;
promise1;
script end;
async1 end;
promise2;
setTimeout
(说明一点: 只能保证在 chrome 浏览器中打印顺序如此, 烟雨仔尝试了诸如 QQ浏览器, 打印顺序有些微差异.)
async function async1() {
console.log("async1 start")
await async2()
console.log("async1 end")
}
async function async2() {
console.log("async2")
}
console.log("start")
setTimeout(() => {
console.log("setTimeout")
})
async1()
new Promise(function(resolve) {
console.log("promise1")
resolve()
}).then(function() {
console.log("promise2")
})
console.log("script end")
过程:
- js 从上至下执行时, 先执行同步代码, 直接打印 start ; async1 start; async2; promise1; script end.
- 这里要说明一下 async/await, await 关键字会等待后面的 promise 结果, 相当于
await Promise.resolve(undefined), 因此打印 async2. 我的理解是await Promise.resolve(undefined)作为微任务, 尚不能立即执行. await 关键字会中断 async1 函数的继续执行, 转而到函数外面继续执行接下来的同步任务. - 同步任务执行完后, 开始执行微任务. 首先执行 await Promise.resolve(undefined), 紧接着就可以打印 async1 end. 之后再执行下一个微任务, 打印 promise2.
- 最后执行宏任务, 也就是 setTimeout.
(如若理解有误, 请各位大佬指点~)
2. node 的 event loop
process.nextTick()
process.nextTick 不属于 event loop 的任一阶段, 并且有自己的队列. nextTick 队列在同步任务结束后执行.
因此在 event loop 过程中, 遇到 process.nextTick 时, 会加入执行栈尾部立即执行, 执行完成后再继续 event loop.
microTask 微任务
从浏览器过来的你, 一定知道 promise 是属于微任务, 因此进入微任务队列.
微任务队列紧随 nextTick 队列执行.
event loop 阶段
node 的 event loop 分为六个阶段, 每个阶段都有自己的 FIFO 队列, 当前阶段的队列用尽, 才会进入下一阶段.
- timers 阶段, 执行 setTimeout 和 setInterval.
该阶段会检查当前时间是否满足定时器的条件, 如果满足就执行(不满足就等到下一次循环的 timers 阶段再进行判断), 进入下一阶段. 请记住这一点! - pending callbacks 阶段, 处理上一轮未执行的 I/O 回调.
- idle, prepare 阶段, 仅 node 内部使用.
- poll 阶段, 获取新的 I/O 事件, 如 fs.readFile().
- check 阶段, 执行 setImmediate()
- close callbacks 阶段, 关闭回调函数, 如 socket.on(“close”)
3. 终极面试题
掌握以下终极面试题, 基本上 event loop 就难不倒我小恐龙了.
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout0')
},0)
setTimeout(function(){
console.log('setTimeout3')
},3)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})
console.log('script end')
script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3
setTimeout0
setTimeout3
setImmediate
-
首先执行同步任务, 遇见相应的异步任务就放入对应的队列或 event loop 阶段.
- 直接打印 script start;
- 将两个 setTimeout 丢进 timers 队列;
- 将 setImmediate 丢进 check 阶段;
- 将 nextTick 丢进 nextTick 队列;
- 执行 async1(), 直接打印 async1 start;
- 执行 async2(), 直接打印 async2, 并返回 Promise.resolve(undefined), 丢进 microTask 队列;
- 由于 await 关键字的存在, 只能停止 async1 的执行;
- 继续进行到 new Promise, 直接打印 promise1 promise2(这里注意, resolve 之后也是会执行的嗷), .then 丢进 microTask 队列.
- 直接打印 script end.
-
同步任务执行结束后, 进入 nextTick 队列, 打印 nextTick.
-
nextTick 结束后, 执行 microTask 队列.
- 首先执行 async1() 中的 Promise.resolve(undefined), 紧随其后有一个同步任务, 直接打印 async1 end.
- 随后打印 .then 里面的 promise3.
-
微任务队列执行完后, 进入 event loop.
- timers 阶段, 判断时间是否满足 setTimeout, 满足就执行, 不满足就放到下一次 timers 中. 于是打印 setTimeout0 setTimeout3.
这里有一个小坑, 放到后面讲. - 跳过 pending callbacks; idle, prepare; poll 阶段;
- 进入 check 阶段,执行 setImmediate(), 打印 setImmediate.
- timers 阶段, 判断时间是否满足 setTimeout, 满足就执行, 不满足就放到下一次 timers 中. 于是打印 setTimeout0 setTimeout3.
setTimeout 和 setImmediate 的执行顺序
setTimeout 和 setImmediate 的执行顺序取决于它们是在哪个阶段被注册的.
-
首先你应该知道的一个点是: setTimeout 时间设置为
0时, node 会强制更改为1ms. -
如果在
最外层或者setImmediate 内部注册两者, 如何执行取决于当时的机器状况.- 以在最外层注册为例, 也就是以上终极面试题的情况, 那么在同步代码执行的同时, 就会将他们分别放入 timers 阶段和 check 阶段.
- 进入 event loop 时先进入timers 阶段, 因此会判断 setTimeout 的执行时间是否满足.
此时如果已经过去了 1ms, 那么 setTimeout 就会执行, 但如果并没有到 1ms, 就会等到下一次 timers 阶段的时候再判断. 这也就是上面提到的坑了, 在该题中, 由于 setTimeout 前面还有很多代码要执行, 可以认为两个 setTimeout 在第一个 timers 阶段就执行了. 但如果前面没有那么多代码要执行, 可想而知 1ms 这个时间就变得关键了.如果第一次 timers 阶段来得很快, 并没有经过 1ms, 那么就会跳过本轮 timers, 先执行 setImmediate(), 到下一轮 timers 阶段再 setTimeout(). 小恐龙瞳孔震惊! - 跳过中间的空队列, 直接进入 check 阶段, 执行 setImmediate() 并打印.
-
如果在
setTimeout或I/O 回调(poll 阶段)里注册两者, 那么必然是先执行 setImmediate().- 以二者注册在 fs.readFile() 方法的回调里面为例, event loop 已经进入到 poll 阶段, 因此
setTimeout 被丢进下一次循环的 timers 里, 而 setImmediate 被丢在本次循环的 check 阶段. - 先到达 check 并执行 setImmediate().
- 随后进入下一次 event loop 的 timers 中执行 setTimeout().
- 以二者注册在 fs.readFile() 方法的回调里面为例, event loop 已经进入到 poll 阶段, 因此
4. 结语
希望本篇文章对读到这里的你有些微帮助. 如果有大佬指点, 感激不尽!
来自烟雨仔~
本文详细探讨了JavaScript中的事件循环机制,包括浏览器和Node.js的事件循环区别。阐述了process.nextTick()、微任务、宏任务(setTimeout等)的执行顺序,并通过实例解析了异步操作如何影响执行流程。最后,文章通过终极面试题来巩固对事件循环的理解,强调了不同场景下setTimeout与setImmediate的执行优先级。
891

被折叠的 条评论
为什么被折叠?



