浏览器事件循环如何处理宏任务?

时间:2019-01-15 09:46:06

标签: javascript google-chrome firefox browser event-loop

我观看了杰克·阿奇博尔德(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

所以问题是为什么处理程序2、3和4与处理程序1在同一帧中执行?

根据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,其中指出一次迭代:

  • 一个宏任务已处理,
  • 所有微任务都已处理
  • 渲染已更新
  • ...(还有其他一些步骤 对此并不重要)

这似乎根本不正确。

2 个答案:

答案 0 :(得分:2)

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

您在代码笔中测量的“帧”是step 10 - Update the rendering之一。 引用规格:

  

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

因此不确定“ ”将以哪个频率触发,但通常为60FPS(大多数监视器以60Hz刷新),因此在这段时间内, a很多事件循环通常会发生迭代。

现在,requestAnimationFrame更加特别,因为如果浏览器认为它有太多事情要做,它可以丢弃 frames 。因此,您的斐波那契很可能会延迟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>

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

那我们听到的“长时间运行” 呢?

我猜您正在谈论"spin the event loop"算法,该算法的确意味着在某些情况下事件循环不会阻塞所有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
 ...
 */

答案 1 :(得分:0)

答案实际上存在于https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

要点:

  1. “框架”中的“框架”表示浏览器在一个事件循环迭代中所做的工作量,例如处理DOM事件,调整大小,滚动,渲染,CSS动画等。 ,即https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

  2. 的所有步骤
  3. “绘画框”的意思是步骤11“更新渲染”部分。

  4. 在事件迭代中何时创建新的“绘画框架”由浏览器确定:

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

    因此,有可能在许多事件迭代(事件/任务处理)之后创建一个新的“绘画框架”。

  5. 对于长任务,再次可能的是,浏览器可能决定不创建新的“画框”。(也许它决定立即彼此处理这些事件,或者不必要创建,因为视图内容不会更改。)