js中的事件循环

本文总结关于js中的事件循环知识。
JS,非阻塞单线程语言,最初就是为了和浏览器交互而生,如果多线程处理DOM就可能会造成冲突引发问题。JS也提供了异步操作如定时器(setTimeout 和 setInterval)、Ajax请求,但异步请求终究还是单线程,因为其执行的时候是将任务放到主线程执行,只有当前面的任务执行完之后才开始执行。准确的说应该是JavaScript的主线程是单线程的,h5添加了 Web Worker,worker 允许一段js程序运行在主线程之外的线程。

浏览器中的事件循环

由上一篇译文js中的宏任务、微任务,可以知道js在执行的过程中会产生执行环境,这些执行环境会被顺序加入到执行栈中,如果遇到异步的代码会被挂起并加入到Task(宏任务或微任务)队列中,一旦执行栈为空,Event Loop就会从Task队列中拿出需要执行的代码并放入执行栈中执行,本质上js的异步还是同步行为。

一次正确的Event loop顺序:

  1. 执行同步代码,这属于宏任务
  2. 执行栈为空,查询是否有微任务需要执行
  3. 执行所有微任务
  4. 必要的话渲染UI
  5. 然后开始下一轮Event loop,继续执行宏任务中的异步代码

微任务包括 process.nextTickpromiseObject.observeMutationObserver

宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI 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 加入就会立即执行。但是我发现用宏任务、微任务来理解更加明显(不知道对不对)。

参考:

  1. 前端进阶之道-Event loop
  2. js中的宏任务、微任务
  3. [js/Nodejs中的异步任务与事件循环](
posted @ 2022-06-20 10:00  EGBDFACE  阅读(99)  评论(0编辑  收藏  举报