一次搞懂Event loop

事件循环

EventLoop

事件循环

事件循环被称作循环的原因在于,它一直在查找新的事件并且执行。一次循环的执行称之为 tick, 在这个循环里执行的代码称作 task

while (eventLoop.waitForTask()) {
  eventLoop.processNextTask()
}

任务(Tasks)中同步执行的代码可能会在循环中生成新的任务。一个简单的生成新任务的编程方式就是 setTimtout(taskFn, deley),当然任务也可以从其他的资源产生,比如用户的事件、网络事件或者DOM的绘制。

event-loop-1.png

任务队列

让事情变得复杂的情况是,事件循环可能有几种任务任务队列。唯一的两个限制是同一个任务源中的事件必须属于同一个队列,并且必须在每个队列中按插入顺序处理任务。除了这些之外,执行环境可以自由地做它所做的事情。例如,它可以决定下一步要处理哪些任务队列。

while (eventLoop.waitForTask()) {
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }
}

基于这个模型,我们失去了对事件执行时间的控制权。浏览器可能决定在执行我们设定的setTimeout之前先清空其他几个队列.

event-loop-2.png

Microtask queue

幸运的是,事件循环也有一个单独的队列叫做 microtask,microtask 将会在百分百在当前task队列执行完毕以后执行

while (eventLoop.waitForTask()) {
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }
}

最简单的方式生成一个 microtask 任务是 Promise.resolve().then(microtaskFn), Microtasks 的插入执行是按照顺序的,而且因为只有一个唯一的 microtask 队列。执行环境不会再搞错执行的时间了。 另外,microtask任务 也可以生成新的 microtask任务 并且插入到同样的队列中(插入当前microtask)并且在同一个 tick 里执行

event-loop-3.png

渲染

最后一个是关于渲染的任务,不同于其他的任务处理,渲染任务并不是被独立的后台任务处理。它可能会是一个独立运行在每一个tick结束后的算法。执行环境拥有较大的选择空间,它可能会在每一个任务队列后执行渲染,也可能执行多个任务队列而不渲染。 幸运的是这里有一个 requestAnimationFrame(handle)函数,它会正确的在下一次渲染时执行内置的函数

最后这就是我们整个的渲染模型

while (eventLoop.waitForTask()) {
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }

  if (shouldRender()) {
    applyScrollResizeAndCSS()
    runAnimationFrames()
    render()
  }
}

event-loop-4.png

以上内容翻译自writing-a-javascript-framework-execution-timing-beyond-settimeout

思考

以上就是对整个event loop的翻译与解释,文章解释比较简洁明细,但是相信大部分同学可能还是不太明白,那么我们换个思路,如果面试官问什么是event loop,面试官是想知道些什么?我应该怎么回答?

event loop顾名思义就是事件循环,为什么要有事件循环呢?因为V8是单线程的,即同一时间只能干一件事情,但是呢文件的读取,网络的IO处理是很缓慢的,并且是不确定的,如果同步等待它们响应,那么用户就起飞了。于是我们就把这个事件加入到一个 事件队列里(task),等到事件完成时,event loop再执行一个事件队列。

值得注意的是,每一种异步事件加入的 事件队列是不一样的。唯一的两个限制是同一个任务源中的事件必须属于同一个队列,并且必须在每个队列中按插入顺序处理任务。 也就是说由系统提供的执行task的方法,如 setTimeout setInterval setimmediate 会在一个task,网络IO会在一个task,用户的事件会在一个task。event-loop将会按照以下顺序执行

  1. update_time 在事件循环的开头,这一步的作用实际上是为了获取一下系统时间,以保证之后的timer有个计时的标准。这个动作会在每次事件循环的时候都发生,确保了之后timer触发的准确性。(其实也不太准确....)
  2. timers 事件循环跑到这个阶段的时候,要检查是否有到期的timer,其实也就是setTimeout和setInterval这种类型的timer,到期了,就会执行他们的回调。
  3. I/O callbacks 处理异步事件的回调,比如网络I/O,比如文件读取I/O。当这些I/O动作都结束的时候,在这个阶段会触发它们的回调。
  4. idle, prepare 这个阶段内部做一些动作,与理解事件循环没啥关系
  5. I/O poll阶段 这个阶段相当有意思,也是事件循环设计的一个有趣的点。这个阶段是选择运行的。选择运行的意思就是不一定会运行。
  6. check 执行setImmediate操作
  7. close callbacks 关闭I/O的动作,比如文件描述符的关闭,链接断开,等等等 (以上参考自方正——Node.js源码解析:深入Libuv理解事件循环

除了task还有一个microtask,这一个概念是ES6提出Promise以后出现的。这个microtask queue只有一个。并且会在且一定会在每一个task后执行,且执行是按顺序的。加入到microtask 的事件类型有Promise.resolve().then(), process.nextTick() 值得注意的是,event loop一定会在执行完micrtask以后才会寻找新的 可执行的task队列。而microtask事件内部又可以产生新的microtask事件比如

(function microtask() {
  process.nextTick(() => microtask())
})()

这样就会不断的在microtask queue添加事件,导致整个eventloop堵塞

最后就是一个渲染的事件队列,这个队列只出现在浏览器上,并且执行环境会根据情况决定执行与否(可能执行很多task queue也不执行渲染队列)。它如果执行则一定会在microtask后执行,通过requestAnimationFrame(handle) 方法,能够保证中间的代码一定能在下一次执行渲染函数前执行

补充常见的产生microtask和task事件的方法

microtasks:

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

tasks:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI渲染

Tips

  1. 我们通过node运行一个js文件,如果没有可执行事件的事件队列,进程就会退出,那么怎么不让它退出呢?

setInterval方法,这货会一直循环建立新的事件,这样能够保证node进程不退出

监听 beforeExit 事件,通过process.on('beforeExit', handle) 这个事件在node进程退出前会触发,但是如果这里面的handle包含了一个可以生成异步事件的操作,则node进程也不会退出。手动触发process.exit(EXIT_CODE)不会触发该事件

  1. setInterval会导致node进程不能正常退出,但是如果希望即使有setInterval也能正常退出怎么办(有一些循环并不希望挂起node进程)?

const timer = process.setInterval(handle, deley) 调用setInterval方法会返回一个timer,调用 timer.unref() 则event-loop判断除它以外,没有可进行的事件队列后也会推出

  1. process.on('exit', handle)中,handle里的异步事件不能执行 exit事件在手动执行process.exit(EXIT_CODE)后,或者event loop中没有可执行的事件队列 时触发。触发 exit 事件后,执行环境就不会再生成新的 事件队列了,因此这里面的异步事件都会被强制队列

最后

以上都是我瞎编的 如果你喜欢我瞎编的文章,欢迎star Github

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术博文

ueditor富文本编辑器 修改框宽度和高度的方法

在使用ueditor的时候,用的textarea <textarea name="content" id="myEditor">这里写这条规则的回复内容</te...

3587
来自专栏Java帮帮-微信公众号-技术文章全总结

错误集锦2.jsp页面syntax error,insert“}”to complete block

补:错误集锦1-HttpServlet was not found on the Java Build Path。 我们在用Eclipse进行Java web开...

3594
来自专栏Android自学

给WordPress文章添加类似说说的状态样式

1743
来自专栏ShaoYL

UIViewController的生命周期及iOS程序执行顺序

29211
来自专栏xiaoxi666的专栏

【开源项目】将图片转换为字符画

请移步Github仓库:https://github.com/xiaoxi666/Img2AsciiVision

1221
来自专栏小狼的世界

PHP处理回车换行时应该注意的一个问题

在我们的数据入库、出库的时候要特别注意这个问题,特别是在进行显示处理的时候,比如使用表单中的 textarea 进行了一段文字的提交,客户端是Windows的话...

861
来自专栏大史住在大前端

webpack4.0各个击破(2)—— CSS篇

以webpack4.0版本为例来演示CSS模块的处理方式,需要用到的插件及功能如下:

1363
来自专栏hbbliyong

JavaScript 调试小技巧

‘debugger;’ 除了console.log,debugger就是另一个我很喜欢的快速调试的工具,将debugger加入代码之后,Chrome会自动在插入...

2907
来自专栏java一日一条

10+ 实用的 JavaScript 调试小技巧

除了console.log,debugger就是另一个我很喜欢的快速调试的工具,将debugger加入代码之后,Chrome会自动在插入它的地方停止,很像C或者...

721
来自专栏黄Java的地盘

提高开发效率之VS Code基础配置篇

VS Code可以通过名为代码片段的功能像编辑器中插入一段指定的文本,具体操作步骤为首选项->用户代码片段->新建全局代码片段。

2472

扫码关注云+社区

领取腾讯云代金券