就目前来讲,
web
应用实现动画的方式有很多种,排除CSS3
自带的transition
和animation
动画属性,这里讲一下JavaScript
实现动画的其中两种方法,requestAnimationFrame
&&setTimeout
。
-
前言
在开始分析之前,我们要了解几个概念,
视觉暂留
屏幕刷新频率
1.视觉暂留
眼睛的另一个重要特是视觉惰,即光象一旦在视网膜上形成,视觉将会对这个光象的感觉维持一个有限的时间,这种生理现象叫做视觉暂留
。对于中等亮度的光刺激,视觉暂留时间约为50ms
至200ms
。当我们看屏幕的时候,虽然你什么也没做,但是屏幕还是以特定的频率在不停刷新,只是这个刷新过程我们肉眼识别到他的细微变化,这就是我们接下来要说的 屏幕刷新频率
2.屏幕刷新频率
我们日常的显示器,一般频率在60Hz
左右,意味着我们的屏幕每1
秒需要刷新60
次,也就是说每1000ms
需要更新60
次的屏幕图像,那么我们由此可以得出,屏幕图像更新一次所需要的时间间隔也就是16.7ms(1000/60≈16.7)
。
由于人的眼睛具有视觉暂留效应
,且暂留时间为50ms
至200ms
,也就是说人在看屏幕的时候,还没等到你的大脑印象消失,电脑屏幕就已经更新了,所以这个间隔让你感觉不到变化。
那么屏幕刷新频率是不是越大越好?我们可以大胆假设一下,假如我有三个显示器,刷新频率分别为1Hz
、60Hz
、200Hz
、那么对应的更新周期时间分别为1000ms
、16.7ms
、5ms
。也就是频率越大,图像更新的间隔就越短,我们看到的画面就会越稳定,当达到一秒更新一次的时候,这个时候我们就能够感觉到明显的屏幕闪烁,带来视觉疲劳。
3.setTimeout
setTimeout
说白了就是个延时计时器,通过设置固定的时间间隔,从而时间一到,执行相应的回调方法。这个过程不会考虑屏幕刷新频率,换句话讲,它的时间是写死的。那么为什么会存在丢帧现象发生,通俗来说就是为什么使用setTimeout
我会感觉到卡顿,画面不稳定。
1.setTimeout
执行的时间与屏幕的刷新频率不一致会导致丢帧现象。我们不考虑异步问题,假设我们现在的屏幕设备是60Hz
的刷新频率。那么我们图像的更新周期也就是16.7ms
,我现在的动画要求是每10ms
往下偏移1px
,那么这个丢帧现象是如何产生的?这里我通过一张图来解释一下。
分析结果:
- 第1次重绘(
16.7ms
):图形偏移到1px
; - 第2次重绘(
33.4ms
):图形偏移到3px
;丢失1px偏移
; - 第3次重绘(
50.1ms
):图形偏移到4px
; - 第4次重绘(
66.8ms
):图形偏移到6px
;丢失1px偏移
; - 第5次重绘(
83.5ms
):图形偏移到8px
;丢失1px偏移
;
......
实验效果:
所以根据分析结果以及实验效果,如果setTimeout
执行的顺序与屏幕的刷新频率不一致,会造成丢帧现象,从而视觉上带给我们的就是不流畅。
2.由于JavaScript
属于单线程,而setTimeout
任务会被放入异步队列,通俗来讲就是它的执行得等一等,具体等什么,不知道,就是想再等等。只有主线程的任务执行完毕之后,才会轮到它去执行,也就是说我虽然设置setTimeout
16.7ms
间隔去执行动画属性改变,但是实际运行的时间可能会有所延迟。这个延迟可能会导致执行的时间与屏幕刷新的时间串掉,造成丢帧。
分析结果:
- 第1次重绘(
16.7ms
):图形未偏移;应该偏移到1px
- 第2次重绘(
33.4ms
):图形偏移到1px
;应该偏移到2px
- 第3次重绘(
50.1ms
):图形偏移到2px;
应该偏移到3px
- 第4次重绘(
66.8ms
):图形偏移到3px
;应该偏移到4px
- 第5次重绘(
83.5ms
):图形偏移到4px
;应该偏移到5px
......
所以根据分析结果,我们可以看出,在异步的现象下,会造成一连串的执行差,从而造成丢帧现象。
4.requestAnimationFrame
官方解释:window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。也就是说requestAnimationFrame
它不需要你去手动设置执行间隔时间,它是跟随系统的屏幕刷新频率走的,如果屏幕刷新频率是60Hz
,那么它的执行间隔就是16.7ms(1000/60≈16.7)
,如果屏幕刷新频率是100Hz
,那么它的执行间隔就是10ms(1000/100=10)
,这样就能够保证它的执行与屏幕的刷新频率保持一致,从而避免丢帧现象。
为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame()
运行在后台标签页或者隐藏的<iframe>
里时,requestAnimationFrame()
会被暂停调用以提升性能和电池寿命。
5.requestAnimationFrame Vs setTimeout 区别
-
requestAnimationFrame
在窗口隐藏的时候,会暂停调用,而setTimeout
不会暂停。 -
requestAnimationFrame
跟随系统屏幕刷新频率,而setTimeout
手动配置,会存在丢帧现象。
代码示例
var start = 0;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function step() {
start++;
element.style.top = start + 'px';
if (start < 500) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
6.附件:requestAnimationFrame Github 兼容降级处理
源地址:https://github.com/darius/requestAnimationFrame
// Adapted from https://gist.github.com/paulirish/1579671 which derived from
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller.
// Fixes from Paul Irish, Tino Zijdel, Andrew Mao, Klemen Slavič, Darius Bacon
// MIT license
if (!Date.now)
Date.now = function() { return new Date().getTime(); };
(function() {
'use strict';
var vendors = ['webkit', 'moz'];
for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
var vp = vendors[i];
window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame']
|| window[vp+'CancelRequestAnimationFrame']);
}
if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) // iOS6 is buggy
|| !window.requestAnimationFrame || !window.cancelAnimationFrame) {
var lastTime = 0;
window.requestAnimationFrame = function(callback) {
var now = Date.now();
var nextTime = Math.max(lastTime + 16, now);
return setTimeout(function() { callback(lastTime = nextTime); },
nextTime - now);
};
window.cancelAnimationFrame = clearTimeout;
}
}());