requestAnimationFrame很困惑

时间:2017-05-02 18:46:12

标签: javascript animation

我开始学习这种方法requestAnimationFrame,但我发现它有点难以理解(至少对我而言)。我知道setInterval / setTimeout是如何工作的,但是当我尝试输入一些简单的代码来测试时:

function message(){console.log('hello');}
requestAnimationFrame(message);

这不会使控制台中的任何动画显示hello消息只出现一次,但是当我在消息函数中调用requestAnmiationFrame(message)时,循环是无穷大的(因此它正在工作)。

但另一方面,我可以使用setInterval / setTimeout在任何地方调用此消息函数,而不必在消息函数内部。

我可以理解,requestAnimationFrame()方法必须在函数内运行吗?或者我应该说在内部使用requestAnimiationFrame()方法时,有一个函数有一个默认的循环用于循环自身的无限时间吗?

问题可能是愚蠢的,但真的希望有人能够启发我。

2 个答案:

答案 0 :(得分:1)

rAF(请求动画帧)和timerssetTimeout / setInterval)之间存在巨大差异。

两者均创建异步运行的任务(您的回调),即浏览器将它们排队并在特定时间点调用它们。
最大的区别在于它们何时获得执行和优先级。

我们需要了解谁在管理Que,以及在什么条件下允许回调运行。

我会尽量保持简单。

同步JavaScript

JavaScript是“单线程”的,这意味着您无法在纯JavaScript中运行异步操作。
浏览器API为您提供了一些方法来异步运行代码:

  • timers
  • rAF
  • DOM个事件
  • promises(这是另一种任务)
  • xhr
  • 等...

使用这些方法之一时,实际上是将一个函数引用传递给浏览器,以在(希望的)所需时间点调用它。

事件循环

浏览器将根据您使用的API来执行此操作,并在任务Que中对您的回调进行队列化处理(承诺除外,这些都是微任务)。

最大的问题是,何时将其传递回“ javascript-land”到调用堆栈中?
这是事件循环的工作,它将检查2个条件:

  1. 调用栈是否为空(意味着当前没有任何函数在“主线程”上运行)。
  2. 是否还有任何代码可在“主线程”上运行。

如果满足这两个条件,它将从Que中提取下一个任务并将其推入调用堆栈。

现在让我们以setTimeout为例。
给出以下代码:

setTimeout(() => console.log('timeout'), 0);
console.log('start');
console.log('end');

setTimout应该等待0ms然后登录timeout,因此我们应该认为日志顺序为:

  1. timeout
  2. start
  3. end

但实际上是:

  1. start
  2. end
  3. timeout

原因是因为我们将回调传递给了浏览器,浏览器等待0ms然后将其推送到任务que。
现在,事件循环的工作就是检查上述两个条件:

  1. 调用栈是否为空(意味着当前没有任何函数在“主线程”上运行)。
  2. 是否还有任何代码可在“主线程”上运行。

两者的答案都是“否”,因为我们仍有2条console.log行要在主要执行上下文上运行。
因此它等待,当我们完成日志时,它将回调从que传递到调用堆栈,然后我们记录timeout

大多数异步回调都以这种方式工作。
请注意promises是不同的,它们是微任务,具有自己的Que,其优先级高于常规回调任务。

好的,所有这些谈话,我还没有回答您的问题。

在回答之前还有一件事。

requestanimationFrame

是另一个浏览器API,可让您在浏览器即将运行渲染步骤之前运行内容:

  1. 样式计算
  2. 布局
  3. 实际打印

因此,例如,如果要更新下一帧上元素的left位置,则可以在此位置进行。

这是可能的实现:

requestAnimationFrame(moveBox)

如果您希望它在每一帧上都保持移动,则应递归调用它:

function callback() {
    moveBox();
    requestAnimationFrame(callback);
}
callback(); 

那为什么或何时在rAF上使用timers

setInterval(moveBox, 0)

这将比rAF实现更快。 它运行得更快的原因是,在浏览器将有机会重新绘制(处理渲染步骤)之前,回调将被多次调用,因此您将多次“更新” left位置,但是浏览器只会重新绘制您传递的最后一个值。
例如,它将从0px跳到15px,而不是一次增加一个像素。

基本上,您可以做的是“猜测”帧之间将流逝多少时间。 最常见的方法是计算“每秒60帧”,因为大多数屏幕都支持该比率。

setInterval(moveBox2, 1000 / 60)  

但是,即使这并不是平滑或一致的,因为它并不是真正的60 fps,最重要的是,当前设备和屏幕的刷新速度可能不如您想象的那样。

因此,用于处理DOM动画,批处理更新和评估内容的最佳API是requestAnimationFrame

运行示例

我举了一个用rAFsetInterval的2个解决方案移动盒子的小例子,希望对您有所帮助。

const showBox2 = location.search.includes('box2');
const showBox3 = location.search.includes('box3');
const box1 = document.getElementById('box1');
const box2 = document.getElementById('box2');
const box3 = document.getElementById('box3');

// move by 1 px
function move1px(el) {
  let left = 0;
  return function() {
    // infinite looping logic - if off screen start over
    if (document.documentElement.clientWidth < left) {
      left = 0;
    } else {
      left++;
    }
    // move left by 1 px
    el.style.left = left + 'px';
  }
}
// raf solution
const moveBox1 = move1px(box1);

function callback() {
  moveBox1();
  requestAnimationFrame(callback);
}
callback();

// setInterval solution
const moveBox2 = move1px(box2);
setInterval(moveBox2, 0);

// setInterval hack
const moveBox3 = move1px(box3);
setInterval(moveBox3, 1000 / 60);
body {
  overflow: hidden;
}

#root {
  display: flex;
  flex-direction: column;
}

.box {
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  height: 30px;
  width: 150px;
  border: 1px solid #333;
  margin: 10px 0;
  color: #fff;
  font-size: 1em;
}

#box1 {
  background-color: slateblue;
}

#box2 {
  background-color: brown;
}

#box3 {
  background-color: green;
}
<div id="root">
  <div class="box" id="box3">setInterval hack</div>
  <div class="box" id="box1">rAF</div>
  <div class="box" id="box2">setInterval</div>
</div>

答案 1 :(得分:0)

requestAnimationFrame实际上并不为您呈现任何内容,它只是将浏览器的呈现循环与您的代码同步,因此您可以使用它以编程方式在屏幕上呈现动画。

它的作用是触发每次浏览器渲染帧时提供的回调函数。这是一个很好的例子,它用于:javascript game