那些被忽略但很好用的 Web API / RequestAnimationFrame

别再用 setTimeOutsetInterval 写动画啦!

如果你有用 js 写过动画,那通常你第一个想到的绝对会是 setTimeOutsetInterval,让画面元素可以在固定的时间间隔进行一点一点的变化,如此就可以形成动画的效果。但其实这两个计时器其实都有一些不为人知的小缺点,而今天要介绍的 RequestAnimationFrame 可以让你不需要担心这些。


RequestAnimationFrame

在正式介绍 RequestAnimationFrame 之前我们要先来了解一些相关概念以及 setTimeOutsetInterval 到底有什麽缺点。

# 萤幕更新频率

大家应该知道,其实动画就是一连串的静态画面以一定的频率连续显示,让人的眼睛及大脑可以脑部成一个动态过程,而这个「一定的频率」到底是多少呢?以现在这个影音世代来说,每秒六十张影格是一个可以让动画看起来最顺畅的。

只也就是为什麽现代萤幕的画面更新率至少都有 60Hz (每秒60帧),当然了,不同的设备、网路环境等因素的影响,萤幕更新率不会都是 60Hz。

 

# setTimeOut

如果依照每秒60帧的需求来使用 setTimeOut 来撰写动画的话,大致上都会是这样写的:

let timerID;
const figure = document.querySelector("#figure")
function moveFigure() {
  figure.style.left = figure.offsetLeft + 5 +"px"
  timerID = setTimeout(moveFigure, 1000 / 60);
}
moveFigure(); // 动画开始
clearTimeout(timerID) // 动画停止

我们透过递回的方式不断的呼叫 setTimeout 来帮我们移动元素,而 1000 / 60 就是用来模拟 60Hz 的频率的。不过使用这样的方法会有以下缺点。

1. Callback 执行通常会晚一点
由於 setTimeout 中的 callback 要等到计时完成後才会被放到伫列(queue)中等待执行,这时候如果堆叠中(stack)还有其他工作项目的话,就必须要等待一些额外的时间才会执行。如果想知道更多细节的话,可以去了解 Event Loop。

2. 与萤幕更新频率对应不上
前面说过,很多因素都会影响萤幕更新率,所以他是一个浮动的频率,但 setTimeout 只能设定固定频率,这时候如果跟更新率对应不上,可能动画就会掉帧的可能。

 

# setInterval

相比 setTimeOut ,可能更多人会用 setInterval,因为它自己就可以不断重复执行 callback,不用搞什麽递回:

const figure = document.querySelector("#figure")
function moveFigure() {
  figure.style.left = figure.offsetLeft + 5 +"px"
}
let timerID = setInterval(moveFigure, 1000 / 60);
moveFigure(); // 动画开始
clearInterval(timerID) // 动画停止

其实除了第一次执行时也会延迟之外,与 setTimeOut 效果没什麽太大差别,所以想当然的 setTimeOut 有的缺点它也都有,而且还额外多了几个:

1. 忽略错误
非常可怕的缺点,尽管你的 Callback 已经发生坏掉了,setInterval 也会义无反顾地执行下去。

2. Callback 有可能被取消
前面有说过当计时完成後 callback 会被放进伫列中,当堆叠空闲时就会被抓出来执行,但要堆叠特别繁忙时,也是有可能连第一次的 callback 都还没被执行,第二次的 callback 就又被放进伫列了,这时候等堆叠空闲时,两次 callback 就会几乎同时执行。

// 堆叠再在处理其他函式
stack = ["其他工作项目"]
queue = []
// 第一次的 setInterval 触发
stack = ["其他工作项目"]
queue = ["第一次 callback"]
// 第二次的 setInterval 触发
stack = ["其他工作项目"]
queue = ["第一次 callback", "第二次 callback"]
// 堆叠空闲了,这时候第一次 callback 会被执行,第二次则紧跟在後。
stack = ["第一次 callback"]
queue = ["第二次 callback"]

为了防止这样的情发生,其实 JS 引擎会在伫列已经有该 SetInterval 的 Callback 的时候,把後面这一次的 Callback 给取消掉。

 

# RequestAnimationFrame

但如果使用今天的主角 RequestAnimationFrame 来制作动画,那上述缺点就通通没有了,因为它会自动与萤幕的更新频率同步,以此来避免掉帧的问题。

let requestID;
const figure = document.querySelector("#figure")
function moveFigure() {
  figure.style.left = figure.offsetLeft + 5 +"px"
  requestID = requestAnimationFrame(moveFigure, 1000 / 60);
}
moveFigure(); // 动画开始
cancelAnimationFrame(requestID) // 动画停止

 

如果大家有兴趣的话,可以把我们今天的三段 code 拿去试试看,我个人是可以感受到非常明显的滑顺感落差,RequestAnimationFrame 所制作出来的动画在视觉上会比较舒服。如果你也有同感的话,你可以开始考虑使用它了!


<<:  Annotation Processor Setup

>>:  DAY19-动态规划(二)

Extra05 - Docker - 容器化

此篇为番外,未收入在本篇的原因是 Docker 是个复杂的工具,因此需要更多的篇幅介绍此工具,但是...

D-04-开始测试 ? mstest ? specflow

撰写测试 相信很多人会想要增加系统的稳定度,但是这该如何做则是个问题,相信很多人看过91 TDD的文...

24.移转 Aras PLM大小事-流程签核动态指派(3)

如果能找出对应的签核主管之後 下面就来解释一下可以设定签核指派的页面 这里我在厂商提供的页面中,新增...

Day 18 - 研习计画之工具评估与协作开发

开发框架 在昨日有提到负责後端的研习生的第一个功课是确定开发的框架,而在评估了一个月之後的考量对象包...

从零开始的8-bit迷宫探险【Level 4】Swift 基础语法 (二)

今日目标 认识完变数、常数、型态之後,接着来认识将资料做集合处理的型别: 阵列 (Array) 字典...