我观看了杰克·阿奇博尔德(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,其中指出一次迭代:
这似乎根本不正确。
答案 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
中要点:
“框架”中的“框架”表示浏览器在一个事件循环迭代中所做的工作量,例如处理DOM事件,调整大小,滚动,渲染,CSS动画等。 ,即https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
“绘画框”的意思是步骤11“更新渲染”部分。
在事件迭代中何时创建新的“绘画框架”由浏览器确定:
此规范不要求任何特定模型来选择渲染机会。但是,例如,如果浏览器尝试达到60Hz的刷新率,则渲染机会最多每60秒(约16.7毫秒)出现一次。如果浏览器发现浏览上下文无法维持此速率,则该浏览上下文可能会下降到每秒更具可持续性的30个渲染机会,而不是偶尔丢帧。同样,如果浏览上下文不可见,则用户代理可能会决定将该页面放慢到每秒4个甚至更低的4个渲染机会。
因此,有可能在许多事件迭代(事件/任务处理)之后创建一个新的“绘画框架”。
对于长任务,再次可能的是,浏览器可能决定不创建新的“画框”。(也许它决定立即彼此处理这些事件,或者不必要创建,因为视图内容不会更改。)