深入理解事件循环

1,138 阅读4分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

JS 引擎并不提供事件循环

在谈事件循环的时候,经常会说请说一说JS的事件循环机制。这常常会让人产生一种误解,事件循环好像是JS语言的特性。

实际上并不是,要弄清楚这个问题,我们先要明晰这些概念。

宿主环境、JS引擎、渲染引擎

宿主环境包含JS引擎和渲染引擎。最常见的宿主环境就是浏览器提供的环境。

  • JS引擎:运行JavaScript代码。最出名的是Chrome的V8引擎。
  • 渲染引擎:对网页进行排版和显示。最出名的是Webkit引擎。

注意,JS引擎运行在JS线程,渲染引擎运行在渲染线程。浏览器让他们互斥,当一个线程运行的时候,另一个线程就得挂起。

除了浏览器,Node.js也是一个主要的宿主环境,这个环境就不包含渲染引擎了。它也提供了同浏览器稍有不同的事件循环机制。

宿主环境提供事件循环

因为JS引擎并不提供事件循环,且关键的一点,JS是单线程语言,所以浏览器需要有一种机制,让JS能处理异步任务,于是有了时间循环机制。

我们来列举一些浏览器有那些异步任务:

  • DOM事件。如鼠标点击产生的回调。
  • http请求产生的回调。
  • setInterval和setTimeout。定时器产生的回调。

这些产生的回调,就会进入事件队列。等JS引擎空闲了,依次的执行它们(但是定时器肯定得等它们到时间了,才执行它们)。

宏任务 和 微任务

上面说的那些,都是宏任务。是宿主让JS拥有了处理异步的能力。但宏任务还是有不足之处的,毕竟是宿主提供的API,涉及跟JS引擎交互,性能上肯定有点问题。同时,JS本身也意识到了自己的不足,区区一个单线程语言,本身没有处理异步的能力。

随着时间的推进,JS语言自身支持异步编程,也就是Promise来了(也就是微任务)!对于JS语言来说,弥补了自己异步能力的缺陷,也为前端开发人员,提供了多一种选择,来处理异步。

这也就是宏任务和微任务的本质:

  • 由宿主提供的,为宏任务。
  • 由JS语言自身提供的,为微任务。

那么,它们的执行时机,解释起来就非常的合理了。在JS代码执行的过程中,产生了一些宏任务和微任务。当前的任务执行结束后,把所有准备好的微任务都执行了,因为它们是JS引擎内部的,先统统执行完,再考虑跟外边交互。等微任务执行完了,好了,去宿主那看看,有没有需要交给我处理的宏任务。

如下图:微任务队列在JS引擎内,宏任务队列在JS引擎外。将准备好的微任务都执行完毕后,再去外边找一个宏任务来执行。

事件循环.png

一次事件循环还做了什么

除了针对JS引擎的部分,事件循环还有针对渲染引擎的部分。

如下图所示,每次事件循环,会检查一下是否需要渲染,如果需要,就把JS引擎挂起,让渲染引擎开始工作,渲染网页,这样整个流程就完整了。

vue的nextTick是宏任务还是微任务

可以是宏任务也可以是微任务。但是默认优先使用微任务。只有在宿主环境不支持微任务的时候,才会回退到使用宏任务。

显然,微任务是更好的选择,拥有更高的执行效率。但也让我产生了疑问,this.$nextTick的回调可以拿到更新后的DOM。更新后的DOM?是渲染后的DOM吗?那岂不是得等一次事件循环结束,浏览器渲染完成页面,才能拿到最新的DOM。那不得用宏任务才行,微任务的执行,并没有完成一次事件循环,新的DOM元素也没有渲染啊。

这里我理解有误,正确的理解应该是这样的,更新DOM是指更新内存中的DOM,并不是指渲染到显示器上。所以,我们用this.$nextTick注册的回调,会同更新DOM的方法,一同顺序的在下一个微任务中执行。是可以拿到更新后的DOM的。

结语

总的来说,事件循环是JS在宿主环境中可以完成任务的重要机制。与JS息息相关,却不是JS语言内的东西。

以上理解是看了多篇文章,加之自己的一点理解总结,感觉逻辑上是合理的,如有问题,欢迎指出。