首页 > 解决方案 > 浏览器事件循环如何处理宏任务?

问题描述

我观看了 Jake Archibald 关于事件循环的演讲 - https://vimeo.com/254947206。根据谈话,我的理解是事件循环将执行尽可能多的宏任务,因为它可以容纳在一帧中,如果有一些长时间运行的宏任务,它将导致帧被跳过。所以我的期望是,任何运行时间长于通常帧持续时间的任务都会导致其他任务在下一帧中执行。我通过创建一个按钮和多个处理程序来测试这一点,例如https://codepen.io/jbojcic1/full/qLggVW

我注意到即使 handlerOne 长时间运行(由于计算密集型斐波那契),处理程序 2、3 和 4 仍然在同一帧中执行。只有 timeoutHandler 在下一帧中被执行。这是我得到的日志:

  animationFrameCallback - 10:4:35:226
  handler one called. fib(40) = 102334155
  handler two called.
  handler three called.
  handler four called.
  animationFrameCallback - 10:4:36:37
  timeout handler called
  animationFrameCallback - 10:4:36:42

所以问题是为什么处理程序二、三和四与处理程序一在同一帧内执行?

根据https://developer.mozilla.org/en-US/docs/Web/API/Frame_Timing_API让事情变得更加混乱,

一帧表示浏览器在一个事件循环迭代中所做的工作量,例如处理 DOM 事件、调整大小、滚动、渲染、CSS 动画等。

为了解释“一个事件循环迭代”,他们链接了https://html.spec.whatwg.org/multipage/webappapis.html#processing-model-8,其中声明在一次迭代中:

这似乎根本不正确。

标签: javascriptgoogle-chromefirefoxbrowserevent-loop

解决方案


您在这里混合了一些概念。

您在 codepen 中测量的“框架”是第 10 步之一 -更新渲染。引用规格:

本规范不强制要求任何特定模型来选择渲染机会。但是例如,如果浏览器试图达到 60Hz 的刷新率,那么渲染机会最多出现在每 60 秒(大约 16.7 毫秒)。如果浏览器发现浏览上下文无法维持此速率,则该浏览上下文可能会下降到更可持续的每秒 30 次渲染机会,而不是偶尔丢帧。类似地,如果浏览上下文不可见,用户代理可能会决定将该页面降低到每秒 4 次甚至更少的渲染机会。

所以不确定这个“”会在哪个频率触发,但一般是60FPS(大多数显示器刷新频率为60Hz),所以在这段时间里,通常会发生很多事件循环迭代。

现在,requestAnimationFrame 更加特别,如果浏览器认为它有太多的事情要执行,它可以丢弃帧。所以你的斐波那契很可能会延迟任何 rAF 回调的执行,直到它完成。


您链接的 MDN 文章所讨论的是PerformanceFrameTiming API领域中的“框架” 。我必须直接承认,我对这个特定的 API 了解不多,并且鉴于它对浏览器的支持非常有限,我认为我们不应该在它上面花太多时间,只是说这无关紧要画框。

我认为我们目前用于测量 EventLoop 迭代的最精确工具是Messaging API
通过创建一个自调用消息事件循环,我们可以挂钩到每个 EventLoop 迭代。

let stopped = false;
let eventloops = 0;
onmessage = e => {
  if(stopped) {
    console.log(`There has been ${eventloops} Event Loops in one anim frame`);
    return;
  }
  eventloops++
  postMessage('', '*');
};
requestAnimationFrame(()=> {
  // start the message loop
  postMessage('', '*');
  // stop in one anim frame
  requestAnimationFrame(()=> stopped = true);
});

让我们看看你的代码在更深层次上是如何表现的:

let done = false;
let started = false;
onmessage = e => {
  if (started) {
    let a = new Date();
    console.log(`new EventLoop - ${a.getHours()}:${a.getMinutes()}:${a.getSeconds()}:${a.getMilliseconds()}`);
  }
  if (done) return;
  postMessage('*', '*');
}

document.getElementById("button").addEventListener("click", handlerOne);
document.getElementById("button").addEventListener("click", handlerTwo);
document.getElementById("button").addEventListener("click", handlerThree);
document.getElementById("button").addEventListener("click", handlerFour);

function handlerOne() {
  started = true;
  setTimeout(timeoutHandler);
  console.log("handler one called. fib(40) = " + fib(40));
}

function handlerTwo() {
  console.log("handler two called.");
}

function handlerThree() {
  console.log("handler three called.");
}

function handlerFour() {
  console.log("handler four called.");
  done = true;
}

function timeoutHandler() {
  console.log("timeout handler called");
}

function fib(x) {
  if (x === 1 || x === 2) return 1
  return fib(x - 1) + fib(x - 2);
}
postMessage('*', '*');
<button id="button">Click me</button>

好的,所以实际上在EventLoop 迭代中有一在事件处理程序和 setTimeout 回调之间触发。我更喜欢它。

但是我们听说过的那个“长期运行的框架”呢?

我猜你说的是“旋转事件循环”算法,它确实是为了让事件循环在某些情况下不会阻塞所有的 UI。

首先,规范只告诉实现者,建议为长时间运行的脚本输入此算法,这不是必须的。

然后,这个算法是允许事件注册和 UI 更新的正常 EventLoop 处理,但任何与 javascript 相关的事情都只是在下一次 EventLoop 迭代时恢复。

所以实际上js没有办法知道我们是否确实进入了这个算法。

甚至我的 MessageEvent 驱动循环也无法分辨,因为在我们退出这个长时间运行的脚本后,事件处理程序才会被推送。

这是一种尝试以更图形化的方式进行的尝试,但存在技术上不准确的风险:

/**
 * ...
 * - handle events
 *    user-click => push([cb1, cb2, cb3]) to call stack
(* - paint if needed (may execute rAF callbacks if any))
 *
 * END OF LOOP
—————————————————————————
 * BEGIN OF LOOP
 *
 * - execute call stack
 *    cb1()
 *      schedule `timeoutHandler`
 *      fib()
 *      ...
 *      ...
 *      ...
 *      ... <-- takes too long => "spin the event loop"
 * [ pause call stack ]
 * - handle events
(* - paint if needed (but do not execute rAF callbacks))
 *
 * END OF LOOP
—————————————————————————
 * BEGIN OF LOOP
 *
 * - execute call stack
 * [ resume call stack ]
 *      (*fib()*)
 *      ...
 *      ...
 *    cb2()
 *    cb3()
 * - handle events
 *   `timeoutHandler` timed out => push to call stack
(* - paint if needed (may execute rAF callbacks if any) )
 *
 * END OF LOOP
—————————————————————————
 * BEGIN OF LOOP
 *
 * - execute call stack
 *   `timeoutHandler`()
 * - handle events
 ...
 */
 

推荐阅读