我可以多次调用requestAnimationFrame吗?

时间:2019-02-06 06:32:59

标签: javascript animation canvas requestanimationframe

两次调用requestAnimationFrame是否正确? 例如:

function animate(){
  // ** Animate here
  if( something )
  requestAnimationFrame(animate);
}

function GameLoop() {
    // ** Some code
    if ( somethingElse )
    animate();
}
setInterval(GameLoop, 1000);

//Main render function
function render(){
  // ** Draw and animate objects
  requestAnimationFrame(render)
}

render();

我认为这样做是不对的。那么如何将所有动画放入一个RAF中?

3 个答案:

答案 0 :(得分:1)

  

我可以多次调用requestAnimationFrame吗?

是的,可以。 requestAnimationFrame按添加顺序执行功能。

在以下情况下,您应该使用一个requestAnimationFrame循环:

  • 性能是应用程序中最重要的内容。
  • 如果您的应用丢帧(或者您担心它可能会在路线图上的某个时候出现)。

这是一个“饱和的” requestAnimationFrame。

在以下情况下,您可以使用多个requestAnimationFrame循环:

  • 您的任务很简单。
  • 您相信您的任务可以在下一帧之前完全运行。

这是一个“不饱和”的requestAnimationFrame。

以在画布上绘制3个圆为例。

在“饱和”代码库中,执行时间很重要。圆圈竞争以获取所需的处理能力。 RAF循环的数量可能是及时完成帧或渲染不完整帧之间的差异。在这种情况下,每秒操作是一个有用的度量。

在我们的“不饱和”代码库中,执行时间无关紧要。圆圈没有竞争以争夺所需的处理能力。在这种情况下,代码无需进一步优化。

完成:JSPerf - RAF Single vs Multiple draw calls Async and Unsaturated

每秒操作数:JSPerf - Single RAF draw calls vs Multiple RAF draw calls

答案 1 :(得分:0)

首先,setInterval和requestAnimation框架都将定期调用动画,每1000毫秒调用一次setInterval,每帧调用requestAnimation。最好只选择两者之一。

使用requestAnimation创建多个循环不一定是错误的,但是大多数人都有一个循环,先调用动画功能再渲染功能。人们会争论性能,但这取决于浏览器如何优化。就个人而言,看到两个不同的循环感觉就像指甲在黑板上刮擦一样,因为我一直见过人们以另一种方式这样做,但是应该可以。

如果您要合并:

function animate(){
    // ** Animate here
    if( something )
}
//Main render function
function render(){
    // ** Draw and animate objects
}

function GameLoop() {
    // ** Some code
    if ( somethingElse )
    animate();
    render();
    requestAnimationFrame(GameLoop);
}


GameLoop();

答案 2 :(得分:0)

requestAnimationFrame(cb)做什么?

它将推送回调cb放入动画帧回调列表中,并将文档标记为动画

然后它将访问此列表,并在单个调用中执行那里的所有回调。 何时发生是基于浏览器的启发式方法,但与绘画框架相对应,这是一个Event Loop应该在其中渲染标记被引发,并标记了何时启动update the rendering算法。
这意味着所有回调将在浏览器有机会绘制到屏幕之前得到执行。

这也意味着可以,您可以“多次调用requestAnimationFrame”。

requestAnimationFrame(funcA);
requestAnimationFrame(funcB);
// ~ same as
requestAnimationFrame((t)=> {
  funcA(t);
  funcB(t);
});

它确实将被包装在相同的执行示例中,就像包装在相同的外部函数中一样,因此没有明显的区别(除了抛出的错误不会阻止下一个回调)。

但这并不意味着您应该...

如果有多个不相关的动画需要同时运行,可以这样做,但是对于同一个动画,同时两次调用requestAnimationFrame通常是一个问题。

很难知道您的回调在堆栈中的何处被添加,从而知道将按什么顺序执行。您可以确定所有开箱即用的操作(来自func = t => requestAnimation(func);循环的递归调用)将先于来自外部的调用(例如用户事件)先执行。 有关更多信息,请参见this Q/A

因此,如果您从外部获得多个可能的条目,您将不知道哪个会先发生=> 您不知道屏幕上显示的内容

在您的情况下,解决方案非常简单:

除了在requestAnimationFrame循环中之外,您不需要从其他任何地方调用render

更彻底地分割逻辑:全局循环将先调用更新函数,然后调用绘制函数。
更新功能将负责更新场景中的所有对象,并且仅根据某些外部信号/变量来更新这些对象。
绘图功能将负责绘制所有应渲染的对象,仅此而已。

这些功能都不应该对其他任何事情负责,您的主循环是引擎。

外部事件只会更新场景对象中的某些变量,但永远不会调用绘图函数,它们将不仅仅负责根据需要启动或停止引擎。

const w = canvas.width = 500;
const h = canvas.height = 300;
const ctx = canvas.getContext('2d');
let invertX = false;

const scene = {
  objects: [],
  update(t) {
    // here we only update the objects
    this.objects.forEach(obj => {
      if(invertX) {
        obj.dx *= -1;
      }
      obj.x += obj.dx;
      obj.y += obj.dy;
      if(obj.x > w) obj.x = (obj.x - w) - obj.w;
      if(obj.x + obj.w < 0) obj.x = w - (obj.x + obj.w);
      if(obj.y > h) obj.y = (obj.y - h) - obj.h;
      if(obj.y + obj.h < 0) obj.y = h - (obj.y + obj.h);
    });
    invertX = false;
  },
  draw() {
    // here we only draw
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,canvas.width, canvas.height);
    this.objects.forEach(obj => {
      ctx.fillStyle = obj.fill;
      ctx.fillRect(obj.x, obj.y, obj.w, obj.h);
    }); 
  }
}

function mainLoop() {
  scene.update();
  scene.draw();
  requestAnimationFrame(mainLoop);
}

for(let i=0; i<50; i++) {
  scene.objects.push({
    x: Math.random() * w,
    y: Math.random() * h,
    w: Math.random() * w / 5,
    h: Math.random() * h / 5,
    dx: (Math.random() * 3 - 1.5),
    dy: (Math.random() * 3 - 1.5),
    fill: '#' + Math.floor(Math.random()*0xFFFFFF).toString(16)
  });
}
// every second do something external
setInterval(() => {
  invertX = true;
}, 1000);
// make one follow the cursor
onmousemove = e => {
  const evtX = e.clientX - canvas.offsetLeft;
  const evtY = e.clientY - canvas.offsetTop;
  const obj = scene.objects[0];
  const dirX = Math.sign(evtX - obj.x);
  const dirY = Math.sign(evtY - obj.y);
  obj.dx = Math.abs(obj.dx) * dirX;
  obj.dy = Math.abs(obj.dy) * dirY;
}

mainLoop();
<canvas id="canvas"></canvas>