图形和动画已经日益成为浏览器中现代 Web 应用程序的必备功能,但实现起来仍然比较困难。视 觉上复杂的功能要求性能调优和硬件加速,不能拖慢浏览器。目前已经有一套日趋完善的 API 和工具可 以用来开发此类功能。
毋庸置疑,是 HTML5 最受欢迎的新特性。这个元素会占据一块页面区域,让 JavaScript 可以动态在上面绘制图片。最早是苹果公司提出并准备用在控制面板中的,随着其他浏览器 的迅速跟进,HTML5 将其纳入标准。目前所有主流浏览器都在某种程度上支持元素。
与浏览器环境中的其他部分一样,自身提供了一些 API,但并非所有浏览器都支持这些 API,其中包括支持基础绘图能力的 2D 上下文和被称为 WebGL 的 3D 上下文。支持的浏览器的最新版 本现在都支持 2D 上下文和 WebGL。
使用requestAnimationFrame
很长时间以来,计时器和定时执行都是 JavaScript 动画最先进的工具。虽然 CSS 过渡和动画方便了 Web 开发者实现某些动画,但 JavaScript 动画领域多年来进展甚微。Firefox 4 率先在浏览器中为 JavaScript 动画增加了一个名为 mozRequestAnimationFrame()方法的 API。这个方法会告诉浏览器要执行动画 了,于是浏览器可以通过最优方式确定重绘的时序。自从出现之后,这个 API 被广泛采用,现在作为 requestAnimationFrame()方法已经得到各大浏览器的支持。
早期定时动画
以前,在 JavaScript 中创建动画基本上就是使用 setInterval()来控制动画的执行。下面的例子展 示了使用 setInterval()的基本模式:
(function() {
function updateAnimations() {
doAnimation1(); doAnimation2(); // 其他任务
}
setInterval(updateAnimations, 100);
})();
动画与 Canvas 图形
作为一个小型动画库的标配,这个 updateAnimations()方法会周期性运行注册的动画任务,并 反映出每个任务的变化(例如,同时更新滚动新闻和进度条)。如果没有动画需要更新,则这个方法既 可以什么也不做,直接退出,也可以停止动画循环,等待其他需要更新的动画。
这种定时动画的问题在于无法准确知晓循环之间的延时。定时间隔必须足够短,这样才能让不同的 动画类型都能平滑顺畅,但又要足够长,以便产生浏览器可以渲染出来的变化。一般计算机显示器的屏 幕刷新率都是 60Hz,基本上意味着每秒需要重绘 60 次。大多数浏览器会限制重绘频率,使其不超出屏 幕的刷新率,这是因为超过刷新率,用户也感知不到。
因此,实现平滑动画最佳的重绘间隔为 1000 毫秒/60,大约 17 毫秒。以这个速度重绘可以实现最平 滑的动画,因为这已经是浏览器的极限了。如果同时运行多个动画,可能需要加以限流,以免 17 毫秒 的重绘间隔过快,导致动画过早运行完。
虽然使用 setInterval()的定时动画比使用多个 setTimeout()实现循环效率更高,但也不是没 有问题。无论 setInterval()还是 setTimeout()都是不能保证时间精度的。作为第二个参数的延时 只能保证何时会把代码添加到浏览器的任务队列,不能保证添加到队列就会立即运行。如果队列前面还 有其他任务,那么就要等这些任务执行完再执行。简单来讲,这里毫秒延时并不是说何时这些代码会执 行,而只是说到时候会把回调加到任务队列。如果添加到队列后,主线程还被其他任务占用,比如正在 处理用户操作,那么回调就不会马上执行。
时间间隔的问题
知道何时绘制下一帧是创造平滑动画的关键。直到几年前,都没有办法确切保证何时能让浏览器把 下一帧绘制出来。随着的流行和 HTML5 游戏的兴起,开发者发现 setInterval()和 setTimeout()的不精确是个大问题。
浏览器自身计时器的精度让这个问题雪上加霜。浏览器的计时器精度不足毫秒。以下是几个浏览器 计时器的精度情况:
IE8 及更早版本的计时器精度为 15.625 毫秒; IE9 及更晚版本的计时器精度为 4 毫秒;
Firefox 和 Safari 的计时器精度为约 10 毫秒; Chrome 的计时器精度为 4 毫秒。
IE9 之前版本的计时器精度是 15.625 毫秒,意味着 0~15 范围内的任何值最终要么是 0,要么是 15, 不可能是别的数。IE9 把计时器精度改进为 4 毫秒,但这对于动画而言还是不够精确。Chrome 计时器精 度是 4 毫秒,而 Firefox 和 Safari 是 10 毫秒。更麻烦的是,浏览器又开始对切换到后台或不活跃标签页 中的计时器执行限流。因此即使将时间间隔设定为最优,也免不了只能得到近似的结果。
requestAnimationFrame
Mozilla 的 Robert O’Callahan 一直在思考这个问题,并提出了一个独特的方案。他指出,浏览器知道 CSS 过渡和动画应该什么时候开始,并据此计算出正确的时间间隔,到时间就去刷新用户界面。但对于 JavaScript 动画,浏览器不知道动画什么时候开始。他给出的方案是创造一个名为 mozRequestAnimationFrame() 的新方法,用以通知浏览器某些 JavaScript 代码要执行动画了。这样浏览器就可以在运行某些代码后进 行适当的优化。目前所有浏览器都支持这个方法不带前缀的版本,即 requestAnimationFrame()。
requestAnimationFrame()方法接收一个参数,此参数是一个要在重绘屏幕前调用的函数。
函数就是修改 DOM 样式以反映下一次重绘有什么变化的地方。为了实现动画循环,可以把多个 requestAnimationFrame()调用串联起来,就像以前使用 setTimeout()时一样:
因为 requestAnimationFrame()只会调用一次传入的函数,所以每次更新用户界面时需要再手 动调用它一次。同样,也需要控制动画何时停止。结果就会得到非常平滑的动画。
使用 requestAnimationFrame 551
目前为止,requestAnimationFrame()已经解决了浏览器不知道 JavaScript 动画何时开始的问题, 以及最佳间隔是多少的问题,但是,不知道自己的代码何时实际执行的问题呢?这个方案同样也给出了 解决方法。 19
function updateProgress() {
var div = document.getElementById("status");
div.style.width = (parseInt(div.style.width, 10) + 5) + "%";
if (div.style.left != "100%") {
requestAnimationFrame(updateProgress);
}
}
requestAnimationFrame(updateProgress);
传给 requestAnimationFrame()的函数实际上可以接收一个参数,此参数是一个 DOMHighRes- TimeStamp 的实例(比如 performance.now()返回的值),表示下次重绘的时间。这一点非常重要: requestAnimationFrame()实际上把重绘任务安排在了未来一个已知的时间点上,而且通过这个参数 告诉了开发者。基于这个参数,就可以更好地决定如何调优动画了。