事件循环:微任务和宏任务
浏览器中 JavaScript 的执行流程和 Node.js 中的流程都是基于 事件循环 的。
理解事件循环的工作方式对于代码优化很重要,有时对于正确的架构也很重要。
在本章中,我们首先介绍有关事件循环工作方式的理论细节,然后介绍该知识的实际应用。
事件循环
事件循环 的概念非常简单。它是一个在 JavaScript 引擎等待任务,执行任务和进入休眠状态等待更多任务这几个状态之间转换的无限循环。
引擎的一般算法:
1.当有任务时:
2.休眠直到出现任务,然后转到第 1 步。
当我们浏览一个网页时就是上述这种形式。JavaScript 引擎大多数时候不执行任何操作,它仅在脚本/处理程序/事件激活时执行。
任务示例:
设置任务 —— 引擎处理它们 —— 然后等待更多任务(即休眠,几乎不消耗 CPU 资源)。
一个任务到来时,引擎可能正处于繁忙状态,那么这个任务就会被排入队列。
多个任务组成了一个队列,即所谓的“宏任务队列”(v8 术语):
例如,当引擎正在忙于执行一段 script 时,用户可能会移动鼠标而产生 mousemove 事件,setTimeout 或许也刚好到期,以及其他任务,这些任务组成了一个队列,如上图所示。
队列中的任务基于“先进先出”的原则执行。当浏览器引擎执行完 script 后,它会处理 mousemove 事件,然后处理 setTimeout 处理程序,依此类推。
到目前为止,很简单,对吧?
两个细节:
以上是理论知识。现在,让我们来看看如何应用这些知识。
用例 1:拆分 CPU 过载任务
假设我们有一个 CPU 过载任务。
例如,语法高亮(用来给本页面中的示例代码着色)是相当耗费 CPU 资源的任务。为了高亮显示代码,它执行分析,创建很多着了色的元素,然后将它们添加到文档中 —— 对于文本量大的文档来说,需要耗费很长时间。
当引擎忙于语法高亮时,它就无法处理其他 DOM 相关的工作,例如处理用户事件等。它甚至可能会导致浏览器“中断(hiccup)”甚至“挂起(hang)”一段时间,这是不可接受的。
我们可以通过将大任务拆分成多个小任务来避免这个问题。高亮显示前 100 行,然后使用 setTimeout(延时参数为 0)来安排(schedule)后 100 行的高亮显示,依此类推。
为了演示这种方法,简单起见,让我们写一个从 1 数到 1000000000 的函数,而不写文本高亮。
如果你运行下面这段代码,你会看到引擎会“挂起”一段时间。对于服务端 JS 来说这显而易见,并且如果你在浏览器中运行它,尝试点击页面上其他按钮时,你会发现在计数结束之前不会处理其他事件。
- let i = 0;
- let start = Date.now();
- function count() {
- // 做一个繁重的任务
- for (let j = 0; j < 1e9; j++) {
- i++;
- }
- alert("Done in " + (Date.now() - start) + 'ms');
- }
- count();
浏览器甚至可能会显示一个“脚本执行时间过长”的警告。
让我们使用嵌套的 setTimeout 调用来拆分这个任务:
- let i = 0;
- let start = Date.now();
- function count() {
- // 做繁重的任务的一部分 (*)
- do {
- i++;
- } while (i % 1e6 != 0);
- if (i == 1e9) {
- alert("Done in " + (Date.now() - start) + 'ms');
- } else {
- setTimeout(count); // 安排(schedule)新的调用 (**)
- }
- }
- count();
现在,浏览器界面在“计数”过程中可以正常使用。
单次执行 count 会完成工作 (*) 的一部分,然后根据需要重新安排(schedule)自身的执行 (**):
现在,如果在引擎忙于执行第一部分时出现了一个新的副任务(例如 onclick 事件),则该任务会被排入队列,然后在第一部分执行结束时,并在下一部分开始执行前,会执行该副任务。周期性地在两次 count 执行期间返回事件循环,这为 JavaScript 引擎提供了足够的“空气”来执行其他操作,以响应其他的用户行为。
值得注意的是这两种变体 —— 是否使用了 setTimeout 对任务进行拆分 —— 在执行速度上是相当的。在执行计数的总耗时上没有多少差异。
为了使两者耗时更接近,让我们来做一个改进。
我们将要把调度(scheduling)移动到 count() 的开头:
- let i = 0;
- let start = Date.now();
- function count() {
- // 将调度(scheduling)移动到开头
- if (i < 1e9 - 1e6) {
- setTimeout(count); // 安排(schedule)新的调用
- }
- do {
- i++;
- } while (i % 1e6 != 0);
- if (i == 1e9) {
- alert("Done in " + (Date.now() - start) + 'ms');
- }
- }
- count();
现在,当我们开始调用 count() 时,会看到我们需要对 count() 进行更多调用,我们就会在工作前立即安排(schedule)它。
如果你运行它,你很容易注意到它花费的时间明显减少了。
为什么?
这很简单:你应该还记得,多个嵌套的 setTimeout 调用在浏览器中的最小延迟为 4ms。即使我们设置了 0,但还是 4ms(或者更久一些)。所以我们安排(schedule)得越早,运行速度也就越快。
最后,我们将一个繁重的任务拆分成了几部分,现在它不会阻塞用户界面了。而且其总耗时并不会长很多。
用例 2:进度指示
对浏览器脚本中的过载型任务进行拆分的另一个好处是,我们可以显示进度指示。
正如前面所提到的,仅在当前运行的任务完成后,才会对 DOM 中的更改进行绘制,无论这个任务运行花费了多长时间。
从一方面讲,这非常好,因为我们的函数可能会创建很多元素,将它们一个接一个地插入到文档中,并更改其样式 —— 访问者不会看到任何未完成的“中间态”内容。很重要,对吧?
这是一个示例,对 i 的更改在该函数完成前不会显示出来,所以我们将只会看到最后的值:
- <div id="progress"></div>
- <script>
- function count() {
- for (let i = 0; i < 1e6; i++) {
- i++;
- progress.innerHTML = i;
- }
- }
- count();
- </script>
9月17日,2020云栖大会上,阿里云正式发布工业大脑3.0。 阿里云智能资深产品专家...
查看表结构,sbtest1有主键、k_1二级索引、i_c二级索引 CREATE TABLE `sbtest1` ...
在TOP云(zuntop.com)科技租赁过服务器的站长都知道独立服务器在价格上比VPS主...
中国最?好的一朵云飘进了华瑞银行。阿里云将进一步助力华瑞银行All in Cloud。 -...
定义 this是函数运行时自动生成的内部对象,即调用函数的那个对象。(不一定很准...
2020年对于云计算行业来说是突破性的一年,因为公共云供应商增加了收入,而疫情...
一、PostgreSQL行业位置 一 行业位置 首先我们看一看RDS PostgreSQL在整个行业当...
最近,DevOps的采用导致了企业计算的重大转变。除无服务器计算,动态配置和即付...
很长时间没有更新原创文章了,但是还一直在思考和沉淀当中,后面公众号会更频繁...
本文转载自网络,原文链接:https://mp.weixin.qq.com/s/vlOUg46B5bcmToX-fjavJQ...