js事件循环:微任务和宏任务

本文详细解释了JavaScript中事件循环的工作原理,介绍了宏任务和微任务的区别,并通过实例展示了如何在浏览器中通过任务拆分避免CPU过载,以及如何利用事件循环机制实现进度指示。
摘要由CSDN通过智能技术生成

浏览器中 JavaScript 的执行流程和 Node.js 中的流程都是基于 事件循环 的。

理解事件循环的工作方式对于代码优化很重要,有时对于正确的架构也很重要。

在本章中,我们首先介绍有关事件循环工作方式的理论细节,然后介绍该知识的实际应用。

事件循环


事件循环 的概念非常简单。它是一个在 JavaScript 引擎等待任务,执行任务和进入休眠状态等待更多任务这几个状态之间转换的无限循环。

引擎的一般算法:

  1. 当有任务时:
  • 从最先进入的任务开始执行。

  • 休眠直到出现任务,然后转到第 1 步。

  • 当我们浏览一个网页时就是上述这种形式。JavaScript 引擎大多数时候不执行任何操作,它仅在脚本/处理程序/事件激活时执行。

任务示例:

  • 当外部脚本 <script src="..."> 加载完成时,任务就是执行它。

  • 当用户移动鼠标时,任务就是派生出 mousemove 事件和执行处理程序。

  • 当安排的(scheduled)setTimeout 时间到达时,任务就是执行其回调。

  • ……诸如此类。

设置任务 —— 引擎处理它们 —— 然后等待更多任务(即休眠,几乎不消耗 CPU 资源)。

一个任务到来时,引擎可能正处于繁忙状态,那么这个任务就会被排入队列。

多个任务组成了一个队列,即所谓的“宏任务队列”(v8 术语):

例如,当引擎正在忙于执行一段 script 时,用户可能会移动鼠标而产生 mousemove 事件,setTimeout 或许也刚好到期,以及其他任务,这些任务组成了一个队列,如上图所示。

队列中的任务基于“先进先出”的原则执行。当浏览器引擎执行完 script 后,它会处理 mousemove 事件,然后处理 setTimeout 处理程序,依此类推。

到目前为止,很简单,对吧?

两个细节:

  1. 引擎执行任务时永远不会进行渲染(render)。如果任务执行需要很长一段时间也没关系。仅在任务完成后才会绘制对 DOM 的更改。

  2. 如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件。因此,在一定时间后,浏览器会抛出一个如“页面未响应”之类的警报,建议你终止这个任务。这种情况常发生在有大量复杂的计算或导致死循环的程序错误时。

以上是理论知识。现在,让我们来看看如何应用这些知识。

用例 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)自身的执行 (**)

  1. 首先执行计数:i=1...1000000

  2. 然后执行计数:i=1000001..2000000

  3. ……以此类推。

现在,如果在引擎忙于执行第一部分时出现了一个新的副任务(例如 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 的更改在该函数完成前不会显示出来,所以我们将只会看到最后的值:

……但是我们也可能想在任务执行期间展示一些东西,例如进度条。

如果我们使用 setTimeout 将繁重的任务拆分成几部分,那么变化就会被在它们之间绘制出来。

这看起来更好看:

现在 div 显示了 i 的值的增长,这就是进度条的一种。

用例 3:在事件之后做一些事情


在事件处理程序中,我们可能会决定推迟某些行为,直到事件冒泡并在所有级别上得到处理后。我们可以通过将该代码包装到零延迟的 setTimeout 中来做到这一点。

在 创建自定义事件[1] 一章中,我们看到过这样一个例子:自定义事件 menu-open 被在 setTimeout 中分派(dispatched),所以它在 click 事件被处理完成之后发生。

menu.onclick = function() {

// …

// 创建一个具有被点击的菜单项的数据的自定义事件

let customEvent = new CustomEvent(“menu-open”, {

bubbles: true

});

// 异步分派(dispatch)自定义事件

setTimeout(() => menu.dispatchEvent(customEvent));

};

宏任务和微任务


除了本章中所讲的 宏任务(macrotask) 外,还有在 微任务队列[2] 一章中提到的 微任务(microtask)

微任务仅来自于我们的代码。它们通常是由 promise 创建的:对 .then/catch/finally 处理程序的执行会成为微任务。微任务也被用于 await 的“幕后”,因为它是 promise 处理的另一种形式。

还有一个特殊的函数 queueMicrotask(func),它对 func 进行排队,以在微任务队列中执行。

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。

例如,看看下面这个示例:

setTimeout(() => alert(“timeout”));

Promise.resolve()

.then(() => alert(“promise”));

alert(“code”);

这里的执行顺序是怎样的?

  1. code 首先显示,因为它是常规的同步调用。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)

最后

一个好的心态和一个坚持的心很重要,很多冲着高薪的人想学习前端,但是能学到最后的没有几个,遇到困难就放弃了,这种人到处都是,就是因为有的东西难,所以他的回报才很大,我们评判一个前端开发者是什么水平,就是他解决问题的能力有多强。

分享一些简单的前端面试题以及学习路线给大家,狂戳这里即可获取!!!

[外链图片转存中…(img-Fs4N4GS3-1711681997491)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-VCVpPURM-1711681997492)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)

最后

一个好的心态和一个坚持的心很重要,很多冲着高薪的人想学习前端,但是能学到最后的没有几个,遇到困难就放弃了,这种人到处都是,就是因为有的东西难,所以他的回报才很大,我们评判一个前端开发者是什么水平,就是他解决问题的能力有多强。

分享一些简单的前端面试题以及学习路线给大家,狂戳这里即可获取!!!

[外链图片转存中…(img-QV58Xwsd-1711681997492)]

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值