Html5 canvas 为什么requestAnimationFrame()在帧的末尾而不是开始处运行我的代码?

Html5 canvas 为什么requestAnimationFrame()在帧的末尾而不是开始处运行我的代码?,html5-canvas,google-chrome-devtools,timeline,frames,requestanimationframe,Html5 Canvas,Google Chrome Devtools,Timeline,Frames,Requestanimationframe,对于这个简单的JSBin() 当一个正方形在屏幕上移动时,Chrome开发工具就是这样做的 时间线看起来: 据我所知,垂直灰色虚线是当前帧的结束和下一帧的开始。在屏幕截图中,我们有一个19.3毫秒的帧,浏览器几乎什么都不做(大量空闲时间)。如果浏览器只是在框架启动时正确运行所有代码,那么它就不能避免这种情况吗 然而,如果我画了500次正方形,在6倍CPU速度的情况下 (),我月经来的时候 浏览器正是我想要的(开始以框架形式运行代码) 启动),但它再次失去同步: 当它开始在虚线上运行时,fps

对于这个简单的JSBin() 当一个正方形在屏幕上移动时,Chrome开发工具就是这样做的 时间线看起来:

据我所知,垂直灰色虚线是当前帧的结束和下一帧的开始。在屏幕截图中,我们有一个19.3毫秒的帧,浏览器几乎什么都不做(大量空闲时间)。如果浏览器只是在框架启动时正确运行所有代码,那么它就不能避免这种情况吗

然而,如果我画了500次正方形,在6倍CPU速度的情况下 (),我月经来的时候 浏览器正是我想要的(开始以框架形式运行代码) 启动),但它再次失去同步:

当它开始在虚线上运行时,fps更加平滑,但我只能在浏览器有一些繁重的工作要做时才能让它工作

那么为什么requestAnimationFrame()每次都不能在帧的开始处触发,我该如何使它触发呢


非常感谢您在这方面给我的任何帮助。

因为这正是
requestAnimationFrame
所做的:它计划在下一个“绘制帧”触发回调,就在实际绘制到屏幕之前

这里的“框架”指的是一个事件循环迭代,而不是一个可视化的迭代,此后我将继续使用“事件循环迭代”来进行区分

因此,如果我们查看HTML规范中描述的,我们可以看到“”算法是从“”算法内部调用的。
该算法负责在第2步通过检查每个活动文档的“呈现机会”来确定当前事件循环迭代是否为绘制迭代。如果不是,则放弃下面的所有内部步骤,包括我们的“运行动画帧回调”。
这意味着我们的
requestAnimationFrame
计划回调只会在一个非常特殊的事件循环迭代中执行:下一个具有渲染机会的事件循环迭代

规格没有精确描述这种“画框”应该出现的频率,但基本上大多数当前的供应商都试图保持60Hz,而Chrome将限制活动显示器的刷新率。预计Chrome的行为会传播到其他供应商


所以你所描述的是正常的。如果您想要一个简化的版本,可以将
requestAnimationFrame(fn)
看作
setTimeout(fn,直到下一个绘制帧所需的时间)
(这里的细微区别是,timedout回调在事件循环迭代的开始执行,而动画帧回调在结束时执行)

为什么要这样设计?

因为大多数时候我们都想在屏幕上显示最新的信息。因此,在绘制之前立即调用这些回调可以确保应该绘制的所有内容都处于最新位置

但这也意味着,事实上,我们不应该冒着失去绘画机会的风险,在那里进行过于繁重的作业


现在,我必须注意到有一个包含
requestPostAnimationFrame
,它将安排回调在下一个“绘制帧”触发,就在实际绘制到屏幕之后
使用此方法,您将获得预期的行为。
不幸的是,这仍然只是一个建议,还没有包括在规范中,而且还不确定是否会包括在内

虽然它已经在Chrome中实现,但在“实验性Web平台功能”标志背后,我们要在普通浏览器中处理它的行为,最好的办法是在下一个事件循环迭代开始时安排回调。
下面是我为以下内容制作的一个示例实现:

if(requestPostAnimationFrame的类型!=“函数”){
monkeyPatchRequestPostAnimationFrame();
}
requestAnimationFrame(animationFrameCallback);
requestPostAnimationFrame(postAnimationFrameCallback);
//猴子贴片后动画框架
//!\ 无法从requestAnimationFrame回调内部调用
函数monkeyPatchRequestPostAnimationFrame(){
console.warn('使用MessageEvent解决方法');
const channel=new MessageChannel();
常量回调=[];
设时间戳=0;
let called=假;
channel.port2.onmessage=e=>{
调用=错误;
const toCall=callbacks.slice();
callbacks.length=0;
toCall.forEach(fn=>{
试一试{
fn(时间戳);
}捕获(e){}
});
}
window.requestPostAnimationFrame=函数(回调){
if(回调类型!==“函数”){
抛出新的TypeError('参数1不可调用');
}
callbacks.push(回调);
如果(!调用){
requestAnimationFrame((时间)=>{
时间戳=时间;
channel.port1.postMessage(“”);
});
调用=真;
}
};
}
//void循环,查看您的开发工具的时间线,以查看每个循环的触发位置
函数animationFrameCallback(){
requestAnimationFrame(animationFrameCallback);
}
函数postAnimationFrameCallback(){
requestPostAnimationFrame(postAnimationFrameCallback)

}
我在最新的chrome浏览器中测试了您的第二个代码剪切,发现结果与第一个相同:


所以请参考@kaido的答案。我认为这是正确的答案。

Hi@kaido。非常感谢您的详细解释。虽然它并没有解决我的问题,但它帮助我更好地理解它,尽管还有一个方面我不能完全理解:使用requestPostAnimationFrame(),我可以让代码在绘制帧后立即运行,但是合成器完成的渲染和绘制仍然会在帧结束时被调用。在我的第二个屏幕截图()中,在我的代码运行后立即调用渲染和绘制(这是我想要的),但我不知道是什么原因导致了这种情况,以及它是否可能影响
var y = 0
canvas.height *= 5
ctx.fillStyle = 'green'
function update () {
  requestAnimationFrame(update)
   ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.fillRect(0, y, 300, 300)
  y++
}
update()