缓动函数requestAnimationFrame 更好的实现浏览器经动画


Posted in Javascript onDecember 07, 2012

写缓动函数用到requestAnimationFrame函数,之前了解过一些,但总觉得又不是很了解,所以翻译一篇老外的文章,以便学习分享。

requestAnimationFrame是什么?
以前我们做动画需要一个定时器,每间隔多少毫秒就做出一些改变。现在有个好消息:浏览器厂商已经决定提供一个专门做动画的方法,即requestAnimationFrame(),而且基于浏览器的层面也能更好的进行优化。但是呢,这只是一个做动画的基础API,即不基于DOM元素的style变化,也不基于canvas,或者WebGL。所以,具体的动画细节需要我们自己写。

我们为什么要用它?
对于同时进行的n个动画,浏览器能够进行优化,把原本需要N次reflow和repaint优化成1次,这样就实现了高质量的动画。举个例子,现在有基于JS的动画,还有基于CSS的transitions,或者SVG SMIL. Plus,如果浏览器的某个tab正在运行这样一个动画,然后你切到另一个tab,或者干脆最小化,总之就是你看不见它了,这时浏览器就会停止动画。这将意味着更少的CPU,GPU和更少的内存消耗,这样电池的使用寿命就大大延长了。

如何使用它?

// shim layer with setTimeout fallback 
window.requestAnimFrame = (function(){ 
return window.requestAnimationFrame || 
window.webkitRequestAnimationFrame || 
window.mozRequestAnimationFrame || 
window.oRequestAnimationFrame || 
window.msRequestAnimationFrame || 
function(/* function */ callback, /* DOMElement */ element){ 
window.setTimeout(callback, 1000 / 60); 
}; 
})(); 
// usage: 
// instead of setInterval(render, 16) .... 
(function animloop(){ 
render(); 
requestAnimFrame(animloop, element); 
})();

注意:这里我使用了“requestAnimFrame”,因为规范仍在不断变化中,我并不想任由规范摆布。
requestAnimationFrame API
window.requestAnimationFrame(function(/* time */ time){ 
// time ~= +new Date // the unix time 
}, /* optional bounding elem */ elem);

先给出Chrome和Firefox的版本
window.mozRequestAnimationFrame([callback]); // Firefox 
window.webkitRequestAnimationFrame(callback[, element]); // Chrome

参数:
callback:(FF可选,Chrome必选)

下次repaint调用的函数,函数的第一个参数是当前时间
element:(FF无)

意译一下吧:其实就是画布了,而那个‘画',是动画。(the element that visually bounds the entire animation)。对canvas和WebGL来说,它就是<canvas>元素,对于DOM节点来说,你可以不管它,如果你想稍微进行一下优化,也可以传个参数进来。

它到底靠不靠谱啊?
现在,Webkit实现(Nightly Safari 和 Chrome Dev Channel 可用)和Mozilla实现(FF4可用)有一些的差异,Mozilla的实现有一个Bug。事实上,FF动画的帧数是这么算的:1000/(16 + N) fps,其中N是callback的执行时间,单位为毫秒。如果你的callback执行时间为1000ms,那么它最高的帧数也就只有1fps。如果你的callback执行时间为1ms,那么帧数差不多就是60fps。这个bug肯定会被修复,也许就是FF4的下一个版本吧。Chrome10没有time参数(added in m11.弱弱的问下,m11是什么?),FF目前没有element参数。
我看了下火狐的那个bug,大概就是说:
FF的mozRequestAnimationFrame()永远不可能达到60fps,即使你的callback执行时间小于1000/60毫秒。举个例子:

function callback(time) { 
window.mozRequestAnimationFrame(callback); 
doWork(); 
}

如果doWork()耗时1000/60毫秒,那么帧数大约是30fps,而同样的动画如果使用setTimeout(callback, 16),帧速则是60fps。似乎callback总是在callback执行完毕后的大约16ms再次开始执行,而不是在callback开始执行后的16ms再次开始执行,如果是后者,且计算又够快的话,就能产生60fps的帧数。
如果你是规范控,传送门在此

话不多说,首先来个经典的动画函数

function animate(element, name, from, to, time) { 
time = time || 800; //默认0.8秒 
var style = element.style, 
latency = 60, // 每60ms一次变化 
count = time / latency, //变化的次数 
step = Math.round((to - from) / count), //每一步的变化量 
now = from; 
function go() { 
count--; 
now = count ? now + step : to; 
style[name] = now + 'px'; 
if (count) { 
setTimeout(go, latency); 
} 
} 
style[name] = from + 'px'; 
setTimeout(go, latency); 
}

姑且不论这个函数的设计存在局限性,如只能对以px为单位的样式进行修改。仅从函数的实现上来看,这可以是一个非常经典的动画理念,其基本逻辑由以下部分组成:
获取起点值from和终点值to,通过动画需要进行的时间time,以及每侦间隔latency的要求,计算出值的改变次数count和每次改变的量step。
开启setTimeout(fn, latency);来步进到下一侦。

在下一侦中,设置属性步进一次,如果动画还没结束,再回到第2步继续下一侦。
这个函数工作得很好,服务了千千万万的站点和系统,事实上jQuery的animate函数的核心也无非是setInterval函数。
但是,随着现在系统复杂度的稳步上升,动画效果也越来越多,同时对动画的流畅度也有了更多的重视,这导致上面的函数会出现一些问题。例如同时打开100个动画效果,根据上面的函数,很明显会有100个定时器在同时运行,这些定时器之间的调度会对性能有轻微的影响。虽然在正常的环境中,这些许的影响并不会有什么关系,但是在动画这种对流畅度有很高要求的环境下,任何细微的影响都可能产生出不好的用户体验。

在这样的情况下,有一些开发者就发明了一种基于统一帧管理的动画框架,他使用一个定时器触发动画帧,不同的动画来注册这些帧,在每一帧上处理多个动画的属性变化。这样的好处是减少了定时器调度的开销,但是对于动画框架的开发者来说,统一帧管理、提供监听帧的API等,都是需要开发和维护的。

浏览器的直接支持
最终,浏览器厂商们发现这件事其实可以由他们来做,并且基于浏览器层面,还可以有更多的优化,比如:
对于一个侦中对DOM的所有操作,只进行一次Layout和Paint。
如果发生动画的元素被隐藏了,那么就不再去Paint。
于是,浏览器开始推出一个API,叫做requestAnimationFrame,关于这个函数,MDC的相关页面有比较详细的介绍,简单来说,这个函数有2种使用方法:
调用requestAnimationFrame函数,传递一个callback参数,则在下一个动画帧时,会调用callback。
不传递参数地直接调用该函数,启动动画帧,下一个帧触发时,会同时触发window.onmozbeforepaint事件,可以通过注册该事件来进行动画。

第2种方法由于依赖于Firefox自己的事件,且beforepaint事件还没进入到标准中,所以不推荐使用,还是使用第1种方式比较好。此时,我们的动画逻辑可以变成这样:
记录当前时间startTime,作为动画开始的时间。
请求下一帧,带上回调函数。
下一帧触发时,回调函数的第一个参数为当前的时间,再与startTime进行比较,确定时间间隔ellapseTime。
判断ellapseTime是否已经超过事先设定的动画时间time,如果超过,则结束动画。
计算动画属性变化的差值differ = to - from,再确定在ellapseTime的时候应该变化多少step = differ / time * ellapseTime。
计算出现在应该变化到的位置Math.round(from + step),并重新对样式赋值。
继续请求下一帧。

新的动画函数
下面就是一个全新的动画函数:

function animate(element, name, from, to, time) { 
time = time || 800; // 默认0.8秒 
var style = element.style, 
startTime = new Date; 
function go(timestamp) { 
var progress = timestamp - startTime; 
if (progress >= duration) { 
style[name] = to + 'px'; 
return; 
} 
var now = (to - from) * (progress / duration); 
style[name] = now.toFixed() + 'px'; 
requestAnimationFrame(go); 
} 
style[name] = from + 'px'; 
requestAnimationFrame(go); 
}

到这一步,还剩一个问题,那就是并不是每个浏览器都支持requestAnimationFrame函数的,所以再做一个简单的修正。
根据Firefox的特性来看,其mozRequestAnimationFrame提供的最高FPS为60,并且会根据每一帧的计算的耗时来进行调整,比如每一帧计算用了1s,那他只会提供1FPS的动画效果。
而Chrome的高版本同样也实现了这个函数,叫webkitRequestAnimationFrame,可以预见未来还会有Opera的oRequestAnimationFrame和IE的msRequestAnimationFrame,所以这里一并做一个简单的兼容处理:
requestAnimationFrame = window.requestAnimationFrame || 
window.mozRequestAnimationFrame || 
window.webkitRequestAnimationFrame || 
window.msRequestAnimationFrame || 
window.oRequestAnimationFrame || 
function(callback) { setTimeout(callback, 1000 / 60); };
Javascript 相关文章推荐
JavaScript 比较时间大小的代码
Apr 24 Javascript
picLazyLoad 实现图片延时加载(包含背景图片)
Jul 21 Javascript
JS实现的幻灯片切换显示效果
Sep 07 Javascript
js中小数向上取整数,向下取整数,四舍五入取整数的实现(必看篇)
Feb 13 Javascript
浅谈js for循环输出i为同一值的问题
Mar 01 Javascript
Angularjs自定义指令实现分页插件(DEMO)
Sep 16 Javascript
jQuery实现打开网页自动弹出遮罩层或点击弹出遮罩层功能示例
Oct 19 jQuery
关于Vue单页面骨架屏实践记录
Dec 13 Javascript
基于 Vue.js 2.0 酷炫自适应背景视频登录页面实现方式
Jan 17 Javascript
Node.JS段点续传:Nginx配置文件分段下载功能的实现方法
Mar 12 Javascript
vue form check 表单验证的实现代码
Dec 09 Javascript
Element-UI 使用el-row 分栏布局的教程
Oct 26 Javascript
javascrpt绑定事件之匿名函数无法解除绑定问题
Dec 06 #Javascript
php图像生成函数之间的区别分析
Dec 06 #Javascript
javascript SpiderMonkey中的函数序列化如何进行
Dec 05 #Javascript
javascript中有趣的反柯里化深入分析
Dec 05 #Javascript
js multiple全选与取消全选实现代码
Dec 04 #Javascript
在js(jquery)中获得文本框焦点和失去焦点的方法
Dec 04 #Javascript
关于javascript中的typeof和instanceof介绍
Dec 04 #Javascript
You might like
phpMyAdmin自动登录和取消自动登录的配置方法
2014/05/12 PHP
php表单文件iframe异步上传实例讲解
2017/07/26 PHP
PHP getDocNamespaces()函数讲解
2019/02/03 PHP
Javascript 对象的解释
2008/11/24 Javascript
window.dialogArguments 使用说明
2011/04/11 Javascript
js中判断数字\字母\中文的正则表达式 (实例)
2012/06/29 Javascript
加载远程图片时,经常因为缓存而得不到更新的解决方法(分享)
2013/06/26 Javascript
js生成随机数之random函数随机示例
2013/12/20 Javascript
js形成页面的一种遮罩效果实例代码
2014/01/04 Javascript
JS遍历数组及打印数组实例分析
2016/01/21 Javascript
快速掌握Node.js之Window下配置NodeJs环境
2016/03/21 NodeJs
jquery $.trim()去除字符串空格的实现方法【附图例】
2016/03/30 Javascript
JQuery控制图片由中心点逐渐放大效果
2016/06/26 Javascript
JavaScript中的Reflect对象详解(ES6新特性)
2016/07/22 Javascript
整理一下常见的IE错误
2016/11/18 Javascript
详解webpack3如何正确引用并使用jQuery库
2017/08/26 jQuery
web前端开发中常见的多列布局解决方案整理(一定要看)
2017/10/15 Javascript
js实时监控文本框输入字数的实例代码
2018/01/18 Javascript
Vue打包部署到Nginx时,css样式不生效的解决方式
2020/08/03 Javascript
解决antd Form 表单校验方法无响应的问题
2020/10/27 Javascript
NodeJS和浏览器中this关键字的不同之处
2021/03/03 NodeJs
Django小白教程之Django用户注册与登录
2016/04/22 Python
python3利用smtplib通过qq邮箱发送邮件方法示例
2017/12/03 Python
Python Pywavelet 小波阈值实例
2019/01/09 Python
python实现烟花小程序
2019/01/30 Python
Python网络爬虫之爬取微博热搜
2019/04/18 Python
Python 占位符的使用方法详解
2019/07/10 Python
Python使用scipy模块实现一维卷积运算示例
2019/09/05 Python
Python 将json序列化后的字符串转换成字典(推荐)
2020/01/06 Python
pytorch 中pad函数toch.nn.functional.pad()的用法
2020/01/08 Python
Python编程快速上手——PDF文件操作案例分析
2020/02/28 Python
python如何保存文本文件
2020/06/07 Python
全网最细 Python 格式化输出用法讲解(推荐)
2021/01/18 Python
计算机应用毕业生自荐信
2013/10/23 职场文书
同意转租证明
2015/06/24 职场文书
python 模拟在天空中放风筝的示例代码
2021/04/21 Python