使用 JavaScript 动画:从 setInterval 到 requestAnimationFrame

动画 DOM 元素[1]画布 的内容是 setInterval 的经典用例。但间隔并不像看起来那样可靠,现在可以使用更合适的 API...

使用 setInterval 动画

要使用 JavaScript 动画一个向右移动 400 像素的元素,基本操作是每隔一段时间移动 10 像素。

JSFiddle 演示.

基于此逻辑的 HTML5 游戏通常以 ~60fps[2] 运行,但如果动画过于复杂或在低规格设备(例如移动电话)上运行,并且处理一帧需要超过 16 毫秒,那么游戏将以更低的帧速率运行:当处理一帧需要 33 毫秒时,游戏以 30fps 运行,游戏元素移动速度是应有速度的两倍。动画看起来仍然很流畅,但游戏体验会发生变化。

以恒定速度动画

要以恒定速度动画,我们需要计算自上一帧以来的时间差,并将元素按比例移动。

使用 requestAnimationFrame 动画

由于间隔参数在复杂动画中无关紧要,因为无法保证它会得到满足,因此设计了一个新的 API:requestAnimationFrame。这只是告诉浏览器“在屏幕上绘制下一帧之前,执行此游戏逻辑/动画处理”的一种方式。浏览器负责选择执行代码的最佳时机,从而更有效地使用资源[3]

以下是使用 requestAnimationFrame 进行动画的写法。
注意:以下代码片段不包含在当前浏览器中运行所需的特性检测和解决方法。如果您想尝试它们,您应该尝试使用 可用的 animLoop.js

处理非活动选项卡

requestAnimationFrame 是针对另一个优势而构建的:让浏览器选择最佳帧间隔允许在非活动选项卡中拥有较长的间隔。用户可以玩一个 CPU 密集型游戏,然后打开一个新选项卡或最小化窗口,游戏将暂停[4],将资源留给其他任务使用。
注意:这种行为对资源和电池使用率的潜在影响是如此积极,以至于浏览器供应商决定将其用于 setTimeout 和 setInterval[5]

这种行为还意味着,当切换回包含动画的选项卡时,计算出的时间差可能非常高。这将导致动画看起来跳跃或产生“虫洞[6]如这里所示

可以通过将时间差限制为最大值或当时间差过高时不渲染帧来修复虫洞。

JSFiddle 演示.

动画队列问题

像 jQuery 这样的库会将元素上的动画排队,以便按顺序执行它们。此队列通常仅用于有意的连续动画。
但是,如果动画由计时器触发,则队列可能会在非活动选项卡中无限增长,因为暂停的动画会堆积在队列中。当切换回受影响的选项卡时,用户会看到大量动画连续播放,而实际上应该在正常间隔内播放一个动画。

JSFiddle 演示.

这个问题在某些自动播放幻灯片(例如 mb.gallery)中可见。为了解决这个问题,开发者可以在触发新动画之前清空动画队列[7]
JSFiddle 演示.

结论

setTimeout 和 setInterval 以及 requestAnimationFrame 的延迟是不可预测的,并且在非活动选项卡中要长得多。在编写动画逻辑、fps 计数器、时间倒计时以及时间测量至关重要的地方,都应该考虑这些因素。

[1] DOM 现在可以使用 CSS3 过渡CSS3 动画 进行动画。
[2] 每 16 毫秒 1 帧是每秒 62.5 帧。
[3] 请参阅 msdn 上的 此事实的说明
[4] requestAnimationFrame 在非活动选项卡中的行为仍在 w3c 中进行开发,并且可能在其他浏览器中有所不同。
[5] 请参阅 相关的 Firefox 错误相关的 chromium 错误
[6] 此术语首先由 Seth Ladd 在其“HTML5 游戏开发简介”演讲中提出。
[7] 请参阅您的 js 库的文档,例如 jQuery 的 effectsstop()

关于 louisremi

开发者关系团队,长期 jQuery 贡献者和开放网络爱好者。 @louis_remi

更多由 louisremi 撰写的文章...


12 条评论

  1. Feross

    这是一篇关于 requestAnimationFrame 的非常棒的概述。谢谢!

    <3 Mozilla Hacks 博客。

    2011 年 8 月 19 日 23:02

  2. Pablo

    很棒的文章,谢谢。

    只有一个问题," + new Date "是什么意思?我从未见过在 new 关键字之前加上加号。

    提前谢谢!
    Pablo

    2011 年 9 月 3 日 04:40

    1. Kris Gray

      它将日期转换为整数并返回该整数,即 new Date().getTime()

      2011 年 9 月 15 日 12:03

  3. Pablo

    非常感谢 Kris!
    在谷歌搜索 " + new Date " 不容易 :(

    Pablo

    2011 年 9 月 16 日 05:59

  4. Chad Elliott

    过去最佳实践是将所有动画放入使用队列的单个循环中。使用此新 API,我不断看到代码对每个动画进行单独的 requestAnimationFrame 调用。浏览器是否会在渲染新帧之前在后台自动排队动画,或者我们应该创建自己的 API 来排队动画?

    以下是我所谈论内容的一个例子。

    http://jsfiddle.net/THEtheChad/RUsnb/

    2012 年 1 月 1 日 13:17

  5. Erik Landvall

    我刚刚创建了一个基于这些原理的 JavaScript。您可以在 github 上找到它:https://github.com/erik-landvall/animator

    2012 年 5 月 21 日 11:22

  6. Ivan Kuckir

    大家好,
    我写了一个基于此原理的“Tweener”:) 请在 tweener.ivank.net 上查看它。

    2012 年 7 月 26 日 00:49

  7. Jens Ahrengot Boddum

    对程序化动画的精彩介绍。如果您问我的话,这是前端开发中最有趣和最具创意的部分之一。

    现在,说到 JavaScript 动画,我觉得我应该提一下 Greensock。它是动画框架世界中的一个新成员,其性能超越了目前可用的所有其他框架。如果您有兴趣,请查看我的教程:Greensock JavaScript 动画

    2012 年 9 月 30 日 14:34

  8. Jens Ahrengot Boddum

    哎呀,链接坏了。以下是链接:“http://ahrengot.com/tutorials/greensock-javascript-animation/

    2012 年 9 月 30 日 14:48

  9. zogzog

    我遇到了以下问题
    我想在 DOM 中的随机元素上显示一个常规的淡出效果。这种效果应该持续时间比间隔时间长。
    我注意到使用 setinterval 时,一个元素上的效果会在另一个效果在另一个 DOM 元素上运行时停止。此外,如果主窗口中存在滚动条,则当我滚动时,所有效果都会消失:当浏览器“繁忙”时,效果会停止或无法启动。
    您知道是怎么回事吗?如何解决这个问题?
    非常感谢

    2012 年 12 月 25 日 05:03

  10. zogzog78

    您好,
    我想使用动画(更改背景颜色)定期更新 DOM 中不同元素的文本。如果我想每秒修改一个 DOM 元素:我使用 setInterval 每秒重复该函数,该函数每秒更改 DOM 中不同元素的值并更改其背景颜色,我发现更改背景颜色的动画(可能持续 3 秒)在每秒处理另一个 DOM 元素时会立即停止。
    如何在处理另一个 DOM 元素时看到 DOM 元素的 3 秒动画?

    2012 年 12 月 25 日 11:23

  11. DC

    太棒了!……谢谢

    2013 年 1 月 19 日 22:36

本文的评论已关闭。