js中的事件循环
本文总结关于js中的事件循环知识。
JS,非阻塞单线程语言,最初就是为了和浏览器交互而生,如果多线程处理DOM就可能会造成冲突引发问题。JS也提供了异步操作如定时器(setTimeout 和 setInterval)、Ajax请求,但异步请求终究还是单线程,因为其执行的时候是将任务放到主线程执行,只有当前面的任务执行完之后才开始执行。准确的说应该是JavaScript的主线程是单线程的,h5添加了 Web Worker,worker 允许一段js程序运行在主线程之外的线程。
浏览器中的事件循环
由上一篇译文js中的宏任务、微任务,可以知道js在执行的过程中会产生执行环境,这些执行环境会被顺序加入到执行栈中,如果遇到异步的代码会被挂起并加入到Task(宏任务或微任务)队列中,一旦执行栈为空,Event Loop就会从Task队列中拿出需要执行的代码并放入执行栈中执行,本质上js的异步还是同步行为。
一次正确的Event loop顺序:
- 执行同步代码,这属于宏任务
- 执行栈为空,查询是否有微任务需要执行
- 执行所有微任务
- 必要的话渲染UI
- 然后开始下一轮Event loop,继续执行宏任务中的异步代码
微任务包括 process.nextTick
,promise
,Object.observe
,MutationObserver
宏任务包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
如果宏任务中的异步代码有大量的计算并且需要操作DOM的情况,为了更快的界面响应可以把操作DOM放入微任务中。
Node中的Event loop
Node 的 Event loop 分为 6 个阶段,每个阶段都有一个 callback queue,只有当一个阶段的 queue 清理干净后才会进入到下一个阶段。它们会按照顺序反复运行
timers:timers 阶段会执行 setTimeout 和 setInterval 的回调,一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的任务而延迟。下限的时间有一个范围:[1,2147483647]
,如果设定的时间不在这个范围,将被设置为 1。
node中 setInterval 的时间不是准确的时间,如果CPU长时间阻塞,事件队列一直没有
I/O:I/O阶段会执行除了 close 事件、定时器和 setImmediate 回调,包括网络、流、tcp的 callback 以及错误等。
idle,prepare:idle,prepare 为node内部实现
poll:在这一阶段中系统会做两件事,
- 执行到点的定时器
- 执行 poll 队列中的事件
当 poll 中没有定时器会发现以下两件事情,
- 如果队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
- 如果队列为空:
- 如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
- 如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调
如果有别的定时器需要被执行,会回到 timer 阶段执行回调。
check:check 阶段执行 setImmediate 的回调。
close callbacks:执行 close 事件,处理关闭的回调如socket.on('close')
。并且在 node 中有些情况下定时器的执行顺序是随机的。
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout
当然在这种情况下,执行顺序是相同的
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// 因为 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,所以会立即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 所以以上输出一定是 setImmediate,setTimeout
上面介绍的都是宏任务的执行情况,微任务会在以上每个阶段完成后立即执行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
Node 中的 process.nextTick
会先于其他微任务执行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
})
// nextTick, timer1, promise1
nextTick 和其他的定时器嵌套
setImmediate(function(){
console.log(1);
process.nextTick(function(){
console.log(4);
})
})
process.nextTick(function(){
console.log(2);
setImmediate(function(){
console.log(3);
})
})
// 2143
虽然 nextTick 在第一次进入事件循环是都会先执行,但如果后续还有 nextTick 加入,node只会在阶段转换时才会去执行,浏览器中则是一有 nextTick 加入就会立即执行。但是我发现用宏任务、微任务来理解更加明显(不知道对不对)。
参考:
- 前端进阶之道-Event loop
- js中的宏任务、微任务
- [js/Nodejs中的异步任务与事件循环](