Why is requestAnimationFrame better than setInterval or setTimeout

为什么我应该使用 requestAnimationFrame 而不是 setTimeout 或 setInterval?

这个自我回答的问题是一个文档示例。

59940 次浏览

High quality animation.

这个问题最简单的回答就是。requestAnimationFrame产生更高质量的动画完全消除闪烁和剪切时可能发生的使用 setTimeoutsetInterval,并减少或完全消除帧跳过。

当一个新的画布缓冲区通过显示扫描中途呈现给显示缓冲区时,由于动画位置不匹配而产生剪切线。

闪烁

当画布缓冲区在画布完全呈现之前呈现给显示缓冲区时引起。

帧跳过

当渲染帧之间的时间不与显示硬件精确同步时引起。每隔这么多帧就会跳过一帧,从而产生不一致的动画。(有一些方法可以减少这种情况,但我个人认为这些方法总体效果更差)由于大多数设备使用60个帧率(或倍数) ,每16.666秒产生一个新帧... ms 和计时器 setTimeoutsetInterval使用整数值,它们永远不能完全匹配帧率(如果你有 interval = 1000/60,四舍五入到17ms)


一个小样胜过千言万语。

更新 问题 请求 AnimationFrame 循环不正确 fps的答案显示 setTimeout 的帧时间是如何不一致的,并将其与 requestAnimationFrame 进行比较。

该演示展示了一个简单的动画(条纹在屏幕上移动) ,点击鼠标按钮将在使用的渲染更新方法之间切换。

使用了几种更新方法。这将取决于您正在运行的硬件设置,以确定动画构件的确切外观。你会在条纹的运动中寻找小的抽搐

Note. You may have display sync turned off, or hardware acceleration off which will affect the quality of all the timing methods. Low end devices may also have trouble with the animation

  • Timer 使用 setTimeout 制作动画,时间为1000/60
  • RAF Best Quality ,使用 request AnimationFrame 进行动画制作
  • 双定时器使用两个定时器,每1000/60清除一个定时器,渲染一个定时器。

    更新 OCT 2019 计时器呈现内容的方式发生了一些变化。为了说明 setInterval与显示器刷新不能正确同步,我改变了双定时器的例子,以说明使用多个 setInterval仍然会导致严重的闪烁。闪烁的程度取决于硬件设置。

  • RAF 使用定时动画 ,使用 requestAnimationFrame,但使用帧运行时间。这种技术在动画中很常见。我相信它是有缺陷的,但我把它留给观众

  • 带有定时动画的定时器 。作为“ RAF 与定时动画”,并在这种情况下使用,以克服帧跳过看到的“定时器”的方法。再一次,我认为这很糟糕,但是游戏社区发誓,当你无法进行显示刷新时,这是最好的方法

/** SimpleFullCanvasMouse.js begin **/


var backBuff;
var bctx;
const STRIPE_WIDTH = 250;
var textWidth;
const helpText = "Click mouse to change render update method.";
var onResize = function(){
if(backBuff === undefined){
backBuff = document.createElement("canvas")    ;
bctx = backBuff.getContext("2d");
        

}
    

backBuff.width = canvas.width;
backBuff.height = canvas.height;
bctx.fillStyle = "White"
bctx.fillRect(0,0,w,h);
bctx.fillStyle = "Black";
for(var i = 0;  i < w; i += STRIPE_WIDTH){
bctx.fillRect(i,0,STRIPE_WIDTH/2,h)   ;
        

}
ctx.font = "20px arial";
ctx.textAlign = "center";
ctx.font = "20px arial";
textWidth = ctx.measureText(helpText).width;
    

};
var tick = 0;
var displayMethod = 0;
var methods = "Timer,RAF Best Quality,Dual Timers,RAF with timed animation,Timer with timed animation".split(",");
var dualTimersActive = false;
var hdl1, hdl2


function display(timeAdvance){  // put code in here


tick += timeAdvance;
tick %= w;




ctx.drawImage(backBuff,tick-w,0);
ctx.drawImage(backBuff,tick,0);
if(textWidth !== undefined){
ctx.fillStyle = "rgba(255,255,255,0.7)";
ctx.fillRect(w /2 - textWidth/2, 0,textWidth,40);
ctx.fillStyle = "black";
ctx.fillText(helpText,w/2, 14);
ctx.fillText("Display method : " + methods[displayMethod],w/2, 34);
}
if(mouse.buttonRaw&1){
displayMethod += 1;
displayMethod %= methods.length;
mouse.buttonRaw = 0;
lastTime = null;
tick = 0;
if(dualTimersActive) {
dualTimersActive = false;
clearInterval(hdl1);
clearInterval(hdl2);
updateMethods[displayMethod]()
}
}
}
















//==================================================================================================
// The following code is support code that provides me with a standard interface to various forums.
// It provides a mouse interface, a full screen canvas, and some global often used variable
// like canvas, ctx, mouse, w, h (width and height), globalTime
// This code is not intended to be part of the answer unless specified and has been formated to reduce
// display size. It should not be used as an example of how to write a canvas interface.
// By Blindman67
const U = undefined;const RESIZE_DEBOUNCE_TIME = 100;
var w,h,cw,ch,canvas,ctx,mouse,createCanvas,resizeCanvas,setGlobals,globalTime=0,resizeCount = 0;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () { var c,cs; cs = (c = document.createElement("canvas")).style; cs.position = "absolute"; cs.top = cs.left = "0px"; cs.zIndex = 1000; document.body.appendChild(c); return c;}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); } canvas.width = window.innerWidth; canvas.height = window.innerHeight; ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); } if (typeof onResize === "function"){ resizeCount += 1; setTimeout(debounceResize,RESIZE_DEBOUNCE_TIME);}
}
function debounceResize(){ resizeCount -= 1; if(resizeCount <= 0){ onResize();}}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; mouse.updateBounds(); }
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0, over : false, bm : [1, 2, 4, 6, 5, 3],
active : false,bounds : null, crashRecover : null, mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.clientX - m.bounds.left; m.y = e.clientY - m.bounds.top;
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
if((m.buttonRaw & 2) && m.crashRecover !== null){ if(typeof m.crashRecover === "function"){ setTimeout(m.crashRecover,0);}}
e.preventDefault();
}
m.updateBounds = function(){
if(m.active){
m.bounds = m.element.getBoundingClientRect();
}
        

}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
m.active = true;
m.updateBounds();
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
m.active = false;
}
}
return mouse;
})();




resizeCanvas();
mouse.start(canvas,true);
onResize()
var lastTime = null;
window.addEventListener("resize",resizeCanvas);
function clearCTX(){
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1;           // reset alpha
ctx.clearRect(0,0,w,h); // though not needed this is here to be fair across methods and demonstrat flicker
}






function dualUpdate(){
if(!dualTimersActive) {
dualTimersActive = true;
hdl1 = setInterval( clearCTX, 1000/60);
hdl2 = setInterval(() => display(10), 1000/60);
}
}
function timerUpdate(){
timer = performance.now();
if(!lastTime){
lastTime = timer;
}
var time = (timer-lastTime) / (1000/60);
lastTime = timer;
setTimeout(updateMethods[displayMethod],1000/60);
clearCTX();
display(10*time);
}
function updateRAF(){
clearCTX();
requestAnimationFrame(updateMethods[displayMethod]);
display(10);
}
function updateRAFTimer(timer){ // Main update loop


clearCTX();
requestAnimationFrame(updateMethods[displayMethod]);
if(!timer){
timer = 0;
}
if(!lastTime){
lastTime = timer;
}
var time = (timer-lastTime) / (1000/60);
display(10 * time);
lastTime = timer;
}


displayMethod = 1;
var updateMethods = [timerUpdate,updateRAF,dualUpdate,updateRAFTimer,timerUpdate]
updateMethods[displayMethod]();


/** SimpleFullCanvasMouse.js end **/