JavaScript中requestAnimationFrame函数的简单介绍 用以实现流畅的动画

JavaScript 中 requestAnimationFrame 函数的简单介绍 用以实现流畅的动画

requestAnimationFrame 是一个让我感到兴奋的玩意,不是因为他有多复杂,而是这名字听起来就足够让人浮想联翩以致于很想去尝试。requestAnimationFrame 函数与动画无关,你可以用它做很多事。

我们来谈谈什么是动画。动画其实是一种假象,是一种不连续的运动以帧的形式呈现给我们的东西。在二十世纪,通常人们观看的电影其实就是通过胶片记录和投影的。它是以每秒至少 24 帧的速度形成的视觉上的运动起来的假象。NTSC 广播的标准的帧速率为 23.975FPS,而 PAL 制式的为 25FPS

因此,至少要以 24FPS 的速率才能形成动画,但这样的动画并不是平滑的,流畅的。平滑的动画要以无线帧速率才能实现,但是对于人类大脑而言是侦测不到那种情况下的帧速率,可以说 60FPS 就已经很不错了。常见的电脑、智能手机等大部分现代化设备通常是以 60FPS 的速率刷新屏幕的,少部分游戏系统则支持 120FPS。

那么,什么又是帧呢?这个没有绝对的定义,它主要是依赖于使用的具体环境。例如,电影胶片的每一帧都是由所记录的 FPS 决定的。在录制视频时,把摄像机的帧率调为 30FPS,那么就必须以 30FPS 的速率在 1s 内播放生成的 30 个单独图像。然而,在讨论 web 时,帧的定义又发生了变化。

对于 web 动画,我们可以在设备屏幕中移动 1px 或者更多。移动一个元素(DOM 元素)的像素越少,那么动画就越流畅,越平滑。帧其实就是 DOM 元素在屏幕上的实时位置的一个快照。在 1s 内,如果一个元素以 1px/次的速度移动 60px,那么 FPS 值就是 60。也就是说,上面等价于以 2px/次的速度移动 120px。虽然移动速度变大了,但是动画并不会更加流畅平滑,因为相应的元素的移动距离也变大了。

那么,如何使用 JavaScript 让 DOM 元素产生动画效果呢?可以使用 JavaScript 中的 setInterval 函数。setInterval 可以以 n 毫秒的间隔时间调用回调函数,而且必须在回调函数的第二个参数传入 n 值。为了实现 60FPS,我们需要以 60 次/s 的速度移动一个元素,那意味着元素必须移动大约 16.7ms(100ms/60frames)。接下来,我们在回调执行中移动 DOM 元素 1px,而且每 16.7ms 需要调用一次回调。

<div id="box"></div>

CSS 代码:

#box {
  width: 50px;
  height: 50px;
  background-color: #000;
}

JavaScript 代码:

var element = document.getElementById('box');
var left = 0;
var animateCallback = function() {
    element.style.marginLeft = (++left) + 'px';
  // clear interval after 60 frame is moved
  if (left == 60) {
    clearInterval(interval);
  }
}
var interval = setInterval(animateCallback, (1000 / 60));

通过上面的代码,我们成功实现了一个平滑的动画效果,但是在 PC 或者手机上面显示时会存在一个很大的问题,并且很难被发现。那就是 setInterval 函数在 n 毫秒后,并不能保证被调用(想了解更多请阅读 Mozilla 文档)。总的说来,setInterval 被看成是延迟执行回调函数的 web API。然而,回调函数总是会被阻塞,这意味着如果网页正忙于处理其它事务,回调就不得不等待,直到栈中的异步任务被清空为止(了解更多,请阅读 how JavaScript works,该链接可以让你了解到栈是什么,以及 web api 工作原理)。不仅如此,回调函数的执行可能会消耗掉比 16.7ms 更长的时间,这就意味着,动画将运行超过 1s(此时,回调执行 60 次),60FPS 也就无法实现。

接下来,我们了解一下什么是失帧?首先,浏览器会以最大 m 次/秒刷新屏幕。数字 m 取决于电脑的屏幕刷新率,浏览器的刷新率,以及 CPU、GPU 的处理能力。如果你的浏览器只能以 30 帧/s 的速度刷新屏幕(由于上面的一个或者多个原因造成),那么以 60 帧/秒的速度运行动画是没有什么意义的,多余的帧数将会消失。与此同时,对 DOM 结构所做的更改要比浏览器渲染的要多,这也被称为布局抖动,因为这些操作是同步的,会影响网站的性能以及绘制操作,从而导致动画效果不佳。

浏览器只在屏幕有样式更改,布局改动,以及回流时才刷新屏幕

此时,需要来自浏览器的某种回调函数,他会告诉我们下一次屏幕刷新的时间,或者更准确的说,是下一次绘制操作将在何时执行。这个回调函数就是 requestAnimationFrame Web API。

作为一个 web api,rAF 将被异步调用。和 setInterval 不一样,requestAnimationFrame 不接收 delay 参数(这里的 delay 指的就是 setInterval 的第二个参数),它只在浏览器准备执行下一次绘制操作时调用回调函数,因此我们要在回调函数中移动 DOM 元素。

让我们看一下前面的 requestAnimationFrame 函数例子:

HTML 代码:

<div id="box"></div>

CSS 代码:

#box{
  width: 50px;
  height: 50px;
  background-color: #000;
}

javascript 代码:

var element = document.getElementById('box');
var left = 0;
var rAF_ID;
var rAFCallback = function(){
    element.style.marginLeft = (++left) + 'px';
  // cancel animation frame after 60px
  if( left == 60 ) {
    cancelAnimationFrame(rAF_ID);
  }else {
      rAF_ID = requestAnimationFrame( rAFCallback );
  }
}
rAF_ID = requestAnimationFrame( rAFCallback );

对比前后 js 代码,我们可能已经注意到了 setInterval 和 requestAnimationFrame 之间的一些差异。首先 rAF 不会在每次每次绘制时自动调用。每次更改元素时都需要发出请求。当浏览器计划进行下一次绘制操作时,这些调用将被一一压栈,并被执行。栈中队列可以在 for 循环,while 循环中或者在更加准确的递归函数中进行。

requestAnimationFrame 返回请求的 id(整数),我们可以使用这个 id 来取消请求,使用 cancelAnimationFrame(id)方法会取消回调的执行,从而停止动画的执行(这里使用的递归)。rAF 并不能保证提供 60FPS 的动画效果,这只是一种避免丢帧以及提高效率的方法,从而帮助获取更多的 FPS 值。这就意味着,如果我们通过使用移动多少像素来取消动画,例如上面例子的 60px,那么根据系统中浏览器刷新率,动画可以持续 1s(60FPS)、2s(120FPS)或者更长时间。

那么,我们究竟应该如何保证在 FPS 为任意值时,我们的动画必须在 1s 内完成,且元素在 1s 内移动 60px 呢?这时候就出现了回调函数参数。

当我们把回调函数参数传递给 rAF 时,rAF 将传递时间戳参数(timestamp),该参数以毫秒(ms)为单位,表示 web 页面加载以来消耗的时间。该时间戳函数给出了调用回调的准确时间。

因此,在我们想出解决这个难题的逻辑之前,我们已经知道了动画的持续时间(1s)和距离(60px),我们需要计算在对应的时间里,我们的 progress(根据上下文理解)有多少,再用这个 progress 乘以 60px,这里的 progress 代表在第一次回调执行以来已经用掉了多少时间。公式如下:

progress = ( starttime - timestamp ) / duration

用 progress 值乘以距离,我们得到了在下一次绘制时 DOM 元素的像素值。

position = distance * progress

一旦 progress 达到了 100%,我们就需要停止调用 requestAnimationFrame 函数。此时可能会出现 progress 超过 100%的情况,出现开始的时间间隔为 980ms(第一次回调操作的时间戳和当前的时间戳之间的差异),下次则可能为 1050ms。

当为 980ms 时,我们不能停止动画,因为如果按照上面的公式,我们还没有完全移动元素,这就是为什么我们需要最小的 progress 值(100)。

公式如下:

safeProgress = Math.min(progress, 1) // 1 == 100%

上面的公式也可能出现因为 progress 的浮点数而造成位置也有浮点数。在这里我们计算的是 css 像素,它与设备像素十分不同(深入阅读请点击链接。css 像素实际上是像素密度值,即在实际设备上渲染一个像素对象(CSS 中提到的像素)需要占用多少像素。因此,我们可以使用 css 像素浮点值,幸运的话我们仍可以在实际设备上通过一些像素来使 DOM 元素移动。代码如下:

<div id="box"></div>

CSS 代码:

#box{
  width: 50px;
  height: 50px;
  background-color: #000;
}

javascript 代码:

var element = document.getElementById('box');
var startTime;
var duration = 1000; // 1 second or 1000ms
var distance = 60; // 60FPS
var rAFCallback = function( timestamp ){
    startTime = startTime || timestamp; // set startTime is null
  var timeElapsedSinceStart = timestamp - startTime;
  var progress = timeElapsedSinceStart / 1000;
  var safeProgress = Math.min( progress.toFixed(2), 1 ); // 2 decimal points
  var newPosition = safeProgress * distance;
  element.style.transform = 'translateX('+ newPosition + 'px)';
  // we need to progress to reach 100%
  if( safeProgress != 1 ){
      requestAnimationFrame( rAFCallback );
  }
}
// request animation frame on render
requestAnimationFrame( rAFCallback );

注意:这里使用 transform 而不是 marginLeft

以上就是 requestAnimationFrame 的全部运行机制,requestAnimationFrame 相较于 setInterval 或者 setTimeout 还有其他优势,就是当浏览器 tab 页面未使用时,requestAnimationFrame 会通过组织 requestAnimationFrame 回调的方式暂停动画,这样既能节省电量又能保留动画的状态。而唯一的不足可能就是它天生的不确定性,我们不知道它何时被调用,但这就是我们必须要面对的。

「点点赞赏,手留余香」

16

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » JavaScript中requestAnimationFrame函数的简单介绍 用以实现流畅的动画

发表回复