说一说我对 JS 事件循环的理解

48 阅读3分钟

单线程的 JS

事件循环赋予了 JavaScript 异步的能力,所以按钮点击事件、文件读取等耗时操作不会阻塞渲染了。
JS 语言本身是单线程的,只在这个单一线程进行耗时操作和页面渲染,必然会发生页面卡顿。事件循环就解决了这个问题。事件循环不是 JS 语言本身中的,而是 JS 运行时中的东西,比如 NodeJS 和 V8 引擎,它们各自都实现了事件循环。

事件循环长什么样

事件循环可以看作是一个一直运行的 while loop,它不断去看队列中有没有事件,有的话就执行它。

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

现在有了事件循环,我们可以把网络请求、按钮点击这些耗时操作都一股脑的放入这个队列中,它们会依次执行。不过,如果我想让按钮点击发生在网络请求之前怎么办?我们继续细化事件循环队列,把它分为宏任务队列和微任务队列,并制定一些优先级规则。

宏任务微任务的优先级

除了宏/微任务,还有一种任务:渲染任务,requestAnimationFrame、回流、重绘就发生在这一阶段。

这三种任务的执行顺序是:

  1. 从宏任务队列中拿一个任务,执行。
  2. 看看微任务队列中有没有任务,依次执行微任务队列中的所有任务。
  3. 现在微任务队列为空了,执行渲染任务,依次执行 requestAnimationFrame,回流,重绘,最后进行 DOM 渲染。

image.png

注意了,在微任务阶段,事件循环会执行所有的微任务事件,如果不断往微任务队列中加入微任务,则会不断执行微任务最后导致卡死。

我们如何往事件循环中加入任务?

如何加入宏任务到宏任务队列?

宏任务可以是来自浏览器的事件,如按钮点击,也可以是我们自己创建的任务。最常用的方法是通过 setTimeout 往宏任务队列中加入任务。

setTimeout 有两个参数:任务和 delay,假如设置 delay 为 1 秒,则过 1 秒后任务会入队宏任务队列,等到事件循环去处理。

如何加入微任务到微任务队列?

下面是一些往微任务队列中加入任务的常用方式:

  • 通过 promise.then 方法将回调函数加入微任务队列
  • 通过 queueMicrotask(任务) 将任务加入微任务队列

事件循环和性能优化

对于我们开发者来说,我们可以通过分析事件循环来发现程序运行的瓶颈,然后进行性能优化。

在 Chrome Dev Tools 中使用 Performance,可以观察到事件循环中都发生了什么。
如下图,灰色的 Task 代表宏任务,橙色的 Run Microtasks 代表微任务。

image.png

References

developer.mozilla.org/en-US/docs/…
javascript.info/event-loop