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

浏览器JavaScript执行流程以及Node.js中的流程均基于事件循环

了解事件循环的工作方式对于优化(有时对于正确的体系结构)非常重要。

在本章中,我们首先介绍有关事物如何工作的理论细节,然后介绍该知识的实际应用。

事件循环

事件循环的概念是很简单的。有一个无限循环,JavaScript引擎等待任务,执行任务,然后休眠,等待更多任务。

引擎的一般算法:

  1. 当有任务:
    • 从最早的任务开始执行它们。
  2. 休眠直到出现任务,然后转到步骤1。

这是浏览页面时看到的形式化信息。JavaScript引擎大部分时间不执行任何操作,仅在脚本/处理程序/事件被激活时才运行。

任务示例:

  • <script src="...">加载外部脚本时,任务是执行它。
  • 用户移动鼠标时,任务是调度mousemove事件并执行处理程序。
  • 当计划好的时间到了时setTimeout,任务是运行其回调。
  • 等等。

设置任务-引擎处理它们-然后等待更多任务(在睡眠时消耗接近零的CPU)。

可能是在引擎繁忙时任务来了,然后才入队了。

这些任务形成一个队列,即所谓的“宏任务队列”(v8术语):

例如,当引擎正忙于执行时script,用户可能会移动鼠标mousemove,这=setTimeout可能是由于任务到期而导致的,等等,这些任务形成了一个队列,如上图所示。

队列中的任务将按照“先到先得”的原则进行处理。当引擎浏览器用完成后script,它将处理mousemove事件,然后setTimeout处理程序,依此类推。

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

还有两个细节:

  1. 引擎执行任务时永远不会进行渲染。任务是否花费很长时间都没关系。仅在任务完成后才绘制对DOM的更改。
  2. 如果一项任务花费的时间太长,浏览器将无法执行其他任务,例如处理用户事件。因此,过了一会儿,它会发出类似“页面无响应”的警报,提示您杀死整个页面的任务。当存在大量复杂的计算或导致无限循环的编程错误时,就会发生这种情况。

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

用例1:拆分占用大量CPU的任务

假设我们有一个需要CPU的任务。

例如,语法高亮(用于着色此页面上的代码示例)相当占用CPU资源。为了突出显示代码,它执行分析,创建许多彩色元素,然后将它们添加到文档中-花费大量时间编写大量文本。

当引擎忙于语法高亮显示时,它无法执行其他与DOM相关的工作,处理用户事件等。它甚至可能导致浏览器“卡顿”甚至“挂起”一小段时间,这是不可接受的。

通过将大任务分成多个部分,我们可以避免问题。突出显示前100行,然后为后100行计划setTimeout(零延迟),依此类推。

为了简单起见,为了演示这种方法,让我们开始一个从11000000000计数的函数。

如果您运行下面的代码,引擎将“挂起”一段时间。对于明显可见的服务器端JS,如果您正在浏览器中运行它,则尝试单击页面上的其他按钮-您会发现在计数结束之前不会处理其他事件。

let i = 0;

let start = Date.now();

function count() {

  // do a heavy job
  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 a piece of the heavy job (*)
  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count); // schedule the new call (**)
  }

}

count();

现在,浏览器界面在“计数”过程中可以正常使用。

一次运行会count完成一部分工作,然后根据需要重新安排自身的时间,例如:

  1. 首次运行计数:i=1...1000000
  2. 第二次运行计数:i=1000001..2000000
  3. …等等。

现在,如果onclick在引擎正忙于执行第1部分时出现新的辅助任务(例如事件),则将其排队,然后在第1部分完成时在下一部分之前执行。count执行之间定期返回事件循环,为JavaScript引擎提供足够的“空气”以执行其他操作,以对其他用户操作做出反应。

值得注意的是,两种变体(无论是否分配工作)setTimeout在速度上都是可比的。总体计数时间没有太大差异。

为了使它们更接近,让我们进行改进。

我们将排程移至的开头count()

let i = 0;

let start = Date.now();

function count() {

  // move the scheduling to the beginning
  if (i < 1e9 - 1e6) {
    setTimeout(count); // schedule the new call
  }

  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  }

}

count();

现在,当我们开始count()发现需要做count()更多的工作时,我们会立即安排工作时间,然后再进行这项工作。

如果您运行它,很容易注意到它花费的时间大大减少了。

为什么?

这很简单:您记得,许多嵌套setTimeout调用在浏览器中的最小延迟为4毫秒。即使我们设置了0,它4ms(或者更多)。因此,我们计划得越早–运行速度越快。

最后,我们将需要大量CPU的任务分成了几个部分–现在它不会阻塞用户界面。而且它的整体执行时间不会更长。

用例2:进度指示

为浏览器脚本分配繁重任务的另一个好处是,我们可以显示进度指示。

如前所述,仅在当前运行的任务完成后才绘制对DOM的更改,而不管它花费多长时间。

一方面,这很棒,因为我们的函数可能会创建许多元素,将它们一个接一个地添加到文档中并更改其样式-访问者不会看到任何“中间”未完成的状态。重要的事情,对不对?

这是演示,在函数完成之前不会显示对i的更改,因此我们将仅看到最后一个值:

<div id="progress"></div>

<script>

  function count() {
    for (let i = 0; i < 1e6; i++) {
      i++;
      progress.innerHTML = i;
    }
  }

  count();
</script>

…但是我们也可能希望在任务执行过程中显示一些东西,例如进度条。

如果我们使用来将繁重的任务分成几部分setTimeout,则更改将在它们之间绘制出来。

这看起来更漂亮:

<div id="progress"></div>

<script>
  let i = 0;

  function count() {

    // do a piece of the heavy job (*)
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e7) {
      setTimeout(count);
    }

  }

  count();
</script>

现在,<div>显示的是的增加值i,这是一种进度条。

用例3:在事件发生后采取措施

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

分派自定义事件一章中,我们看到了一个示例:自定义事件menu-open是在setTimeout中分派的,因此它在完全处理“ click”事件之后发生。

menu.onclick = function() {
  // ...

  // create a custom event with the clicked menu item data
  let customEvent = new CustomEvent("menu-open", {
    bubbles: true
  });

  // dispatch the custom event asynchronously
  setTimeout(() => menu.dispatchEvent(customEvent));
};

宏任务和微任务

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

还有一个特殊功能queueMicrotask(func)func可在微任务队列中排队等待执行。

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

例如,看一下:

setTimeout(() => alert("timeout"));

Promise.resolve()
  .then(() => alert("promise"));

alert("code");

这将是什么顺序?

  1. code 首先显示,因为它是常规的同步调用。
  2. promise显示第二个,因为它.then通过微任务队列,并在当前代码之后运行。
  3. timeout 最后显示,因为它是一个宏任务。

更丰富的事件循环图片如下所示(顺序是从上到下,即:首先是脚本,然后是微任务,渲染等):


在执行任何其他事件处理或呈现或执行任何其他宏任务之前,所有微任务都已完成。

这很重要,因为它可以确保微任务之间的应用程序环境基本相同(没有鼠标坐标更改,没有新的网络数据等)。

如果我们想异步执行一个函数(在当前代码之后),但是在呈现更改或处理新事件之前,可以使用进行调度queueMicrotask

这是一个带有“计数进度条”的示例,与之前显示的示例类似,但queueMicrotask用于代替setTimeout。您可以看到它在最后渲染。就像同步代码一样:

<div id="progress"></div>

<script>
  let i = 0;

  function count() {

    // do a piece of the heavy job (*)
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e6) {
      queueMicrotask(count);
    }

  }

  count();
</script>

概括

更详细的事件循环算法(尽管与规范相比仍简化了):

  1. 宏任务队列中出队并运行最早的任务(例如“脚本”)。
  2. 执行所有微任务
    • 当微任务队列不为空时:
      • 出队并运行最早的微任务。
  3. 渲染更改(如果有)。
  4. 如果宏任务队列为空,请等待直到出现宏任务。
  5. 转到步骤1。

要安排新的宏任务

  • 使用零延迟setTimeout(f)

这可以用于将繁重的计算任务分解为多个部分,以使浏览器能够对用户事件做出反应并显示它们之间的进度。

另外,在事件处理程序中用于安排事件完全处理(冒泡完成)后的操作。

安排新的微任务

  • 使用queueMicrotask(f)
  • 诺言处理程序还会通过微任务队列。

微任务之间没有UI或网络事件处理:它们立即一个接一个地运行。

因此,您可能想queueMicrotask异步执行功能,但要在环境状态下执行。

Web Worker
对于不应该阻塞事件循环的长时间繁琐的计算,我们可以使用Web Workers。
这是在另一个并行线程中运行代码的方式。
Web Workers可以与主进程交换消息,但是它们具有自己的变量和事件循环。
Web Worker没有访问DOM的权限,因此它们对于同时使用多个CPU内核的计算非常有用。

参考

Event loop: microtasks and macrotasks

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容