我观看了 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 让事情变得更加困惑,
A frame represents the amount of work a browser does in one event loop iteration such as processing DOM events, resizing, scrolling, rendering, CSS animations, etc.
并解释“一个事件循环迭代”,他们链接 https://html.spec.whatwg.org/multipage/webappapis.html#processing-model-8其中指出在一次迭代中:
- 处理一个宏任务,
- 所有微任务均已处理
- 渲染已更新
- ...(还有一些其他步骤,例如 对此并不重要)
这似乎根本不正确。
最佳答案
您在这里混合了一些概念。
您在代码笔中测量的“框架”是 step 10 - Update the rendering 之一。 引用规范:
This specification does not mandate any particular model for selecting rendering opportunities. But for example, if the browser is attempting to achieve a 60Hz refresh rate, then rendering opportunities occur at a maximum of every 60th of a second (about 16.7ms). If the browser finds that a browsing context is not able to sustain this rate, it might drop to a more sustainable 30 rendering opportunities per second for that browsing context, rather than occasionally dropping frames. Similarly, if a browsing context is not visible, the user agent might decide to drop that page to a much slower 4 rendering opportunities per second, or even less.
所以不确定这个“帧”会以什么频率触发,但通常是 60FPS(大多数显示器以 60Hz 刷新),所以在这段时间内,通常会发生很多事件循环迭代。
现在,requestAnimationFrame 更加特别,因为如果浏览器认为它有太多事情需要执行,它可以丢弃帧。因此,您的斐波那契数很可能会延迟 rAF 回调的执行,直到完成为止。
<小时/>您链接的 MDN 文章所讨论的是 PerformanceFrameTiming API 领域中的“框架” 。我必须直接承认,我对这个特定的 API 没有太多的了解,并且考虑到它的浏览器支持非常有限,我认为我们不应该在它上面花太多时间,除非说这无关。带画框。
我认为我们目前用于测量 EventLoop 迭代的最精确的工具是 Messaging API .
通过创建自调用消息事件循环,我们可以 Hook 每个 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>
好的,实际上在事件处理程序和 setTimeout 回调之间触发一个帧,就像在 EventLoop 迭代 中一样。我更喜欢它。
但是我们听说过的“长时间运行的帧”又如何呢?
我猜你说的是 "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
...
*/
关于javascript - 浏览器事件循环如何处理宏任务?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54196299/