浅谈 Event loop (事件循环)

时间:2024-06-16 13:34:38

从Event Loop谈JS的运行机制

先来理解一个概念:

  • JS分为同步任务和异步任务

  • 同步任务都在主线程上执行,形成一个执行栈 Execute Content Stack

  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列(根据MDN准确的说应该是消息队列 message queue)之中放置一个消息(一般都是异步的回调函数)。

  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取消息队列,将可运行的异步任务添加到可执行栈中,开始执行。

看图:

浅谈 Event loop (事件循环)

看到这里,应该就可以理解了:为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到消息队列时,主线程还不空闲,正在执行其它代码,所以自然有误差。

事件循环机制进一步补充

这里就直接引用一张图片来协助理解:(参考自Philip Roberts的演讲《Help, I’m stuck in an event-loop》)

浅谈 Event loop (事件循环)

上图大致描述就是:

  • 主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)

  • 而栈中的代码执行完毕,就会读取事件队列中的事件到执行栈中,去执行那些回调

  • 如此循环

  • 注意,总是要等待栈中的代码执行完毕后才会去读取消息队列中的消息

然后我们通过阅读Promise/A+规范,可以得知JS中分为两种任务类型,分别是macro-task和micro-task。

Macro-tasks包括: script(整体代码)、setTimeout, setInterval, setImmediate, I/O, UI Rendering;(可以看到,事件队列中的每一个事件都是一个macrotask)

Micro-tasks包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver。

总结下运行机制:

  • 执行一个宏任务(栈中没有就从事件队列中获取)

  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中

  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)

  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

如图:

浅谈 Event loop (事件循环)

 这里注意的是,UI Rendering是在micro-task之后执行,需要在UI渲染之前执行的逻辑,一般采用micro-task异步回调方式进行调用。 同样,micro-task队列不宜过长,给micro-task队列添加过多回调阻塞macro-task队列的任务执行是小事,重点是这有可能会阻塞UI Render,导致页面不能更新。浏览器也会基于性能方面的考虑,对micro-task中的任务个数进行限制。

参考文章: