requestAnimationFrame为什么比setInterval或setTimeout更好?

124

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

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


您也可以查看这个链接 - Redu
现在还有 requestIdleCallback: https://developer.chrome.com/blog/using-requestidlecallback/ - undefined
2个回答

160

高质量的动画。

简单来说,使用requestAnimationFrame可以产生更高质量的动画效果,完全消除了使用setTimeoutsetInterval时可能出现的闪烁和剪切,并减少或完全消除帧跳过的情况。

剪切

是指在显示扫描的中途向显示缓冲区呈现新的画布缓冲区,导致由于动画位置不匹配而引起的错位线。

闪烁

是由于在画布完全渲染之前将画布缓冲区呈现给显示缓冲区造成的。

帧跳过

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


演示效果胜过千言万语。

更新问题的答案requestAnimationFrame loop not correct fps显示setTimeout的帧时间不一致,并将其与requestAnimationFrame进行比较。

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

使用了几种更新方法。具体的动画伪像外观取决于您运行的硬件设置。您将寻找条纹移动中的小抖动。

注意。您可能已关闭显示同步或关闭硬件加速,这将影响所有计时方法的质量。低端设备可能也无法处理动画。

  • 计时器 使用setTimeout进行动画,时间为1000/60
  • RAF最佳质量,使用requestAnimationFrame进行动画
  • 双定时器使用两个定时器,一个每1000/60调用清除,另一个用于渲染。

    2019年10月更新,定时器呈现内容的方式有所变化。为了表明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 **/


2
好的回答,需要注意的是浏览器会自动为您执行双缓冲,因此使用普通画布时永远不会出现剪切的风险,同样适用于闪烁。 - XCS
4
@Cristy 是的,DOM 双缓冲。然而,如果你使用单个函数进行渲染,则在该函数退出后(执行空闲 aka 调用栈为空),立即将后备缓冲区呈现给显示RAM,这可能正在进行中扫描。如果您使用两个函数进行渲染,两者都退出到空闲状态,这将导致动画出现剪切,闪烁。requestAnimationFrames 的回调是特殊的,当退出时,后备缓冲区会保持直到Vsync(没有像素移动到显示器)。这可以防止剪切和闪烁。 - Blindman67
我在所有的示例中都看到了明显的锯齿,线条看起来像楼梯。然而动画很平滑,并且似乎没有跳帧现象。有没有办法在不降低帧率的情况下纠正这个问题?(Chrome v84,糟糕的老联想电脑,甚至还有更差的显卡。) - Victor Stoddard
@VictorStoddard,请检查显卡设备设置。确保它没有设置为覆盖V sync。 - Blindman67
1
我不明白我应该看到什么。所有示例中,我看到条形图边缘出现扭曲,尽管在非rAF示例中扭曲似乎更加夸张,这可能是因为条形图移动得比rAF示例快得多。为什么所有示例中的条形图速度不都相同,以便进行苹果与苹果的比较呢? - KevinHJ

0
当我们使用setTimeout或setInterval时,这是多余的,因为setTimeout或setInterval执行了一个任务,其中只有最后一个是必要的,所以前面的3个是多余的,没有用处,只会消耗更多的CPU资源。而requestAnimationFrame只执行一步,所以我们可以粗略地说requestAnimationFrame比setTimeout或setInterval好了近4倍。

为什么这是多余的?除了任何YouTube视频之外,你有任何可靠的来源吗? - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接