使用requestAnimationFrame实现js动画性能好


Posted in Javascript onAugust 06, 2015

使用requestAnimationFrame实现js动画性能好。先给大家简单介绍下requestAnimationFrame比起setTimeout、setInterval有哪些优势?

示例一:

requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:
1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。

像setTimeout、setInterval一样,requestAnimationFrame是一个全局函数。调用requestAnimationFrame后,它会要求浏览器根据自己的频率进行一次重绘,它接收一个回调函数作为参数,在即将开始的浏览器重绘时,会调用这个函数,并会给这个函数传入调用回调函数时的时间作为参数。由于requestAnimationFrame的功效只是一次性的,所以若想达到动画效果,则必须连续不断的调用requestAnimationFrame,就像我们使用setTimeout来实现动画所做的那样。requestAnimationFrame函数会返回一个资源标识符,可以把它作为参数传入cancelAnimationFrame函数来取消requestAnimationFrame的回调。怎么样,是不是也跟setTimeout的clearTimeout很相似啊。
所以,可以这么说,requestAnimationFrame就是一个性能优化版、专为动画量身打造的setTimeout,不同的是requestAnimationFrame不是自己指定回调函数运行的时间,而是跟着浏览器内建的刷新频率来执行回调,这当然就能达到浏览器所能实现动画的最佳效果了。
目前,各个支持requestAnimationFrame的浏览器有些还是自己的私有实现,所以必须加前缀,对于不支持requestAnimationFrame的浏览器,我们只能使用setTimeout,因为两者的使用方式几近相同,所以这两者的兼容并不难。对于支持requestAnimationFrame的浏览器,我们使用requestAnimationFrame,而不支持的我们优雅降级使用传统的setTimeout。把它们封装一下,就能得到一个统一兼容各大浏览器的API了。
代码可以到这里来查看:https://gist.github.com/chaping/88813f56e75b0fd43f8c

var lastTime = 0;
var prefixes = 'webkit moz ms o'.split(' '); //各浏览器前缀
var requestAnimationFrame = window.requestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame;
var prefix;
//通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
for( var i = 0; i < prefixes.length; i++ ) {
 if ( requestAnimationFrame && cancelAnimationFrame ) {
 break;
 }
 prefix = prefixes[i];
 requestAnimationFrame = requestAnimationFrame || window[ prefix + 'RequestAnimationFrame' ];
 cancelAnimationFrame = cancelAnimationFrame || window[ prefix + 'CancelAnimationFrame' ] || window[ prefix + 'CancelRequestAnimationFrame' ];
}
//如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
if ( !requestAnimationFrame || !cancelAnimationFrame ) {
 requestAnimationFrame = function( callback, element ) {
 var currTime = new Date().getTime();
 //为了使setTimteout的尽可能的接近每秒60帧的效果
 var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ); 
 var id = window.setTimeout( function() {
 callback( currTime + timeToCall );
 }, timeToCall );
 lastTime = currTime + timeToCall;
 return id;
 };
 cancelAnimationFrame = function( id ) {
 window.clearTimeout( id );
 };
}
//得到兼容各浏览器的API
window.requestAnimationFrame = requestAnimationFrame; 
window.cancelAnimationFrame = cancelAnimationFrame;

这样子我们就能在所有浏览器上使用requestAnimationFrame和cancelAnimationFrame了。
下面举个简单的例子来说明怎么运用requestAnimationFrame进行动画,下面的代码会将id为demo的div以动画的形式向右移动到300px

<div id="demo" style="position:absolute; width:100px; height:100px; background:#ccc; left:0; top:0;"></div>
<script>
var demo = document.getElementById('demo');
function rander(){
 demo.style.left = parseInt(demo.style.left) + 1 + 'px'; //每一帧向右移动1px
}
requestAnimationFrame(function(){
 rander();
 //当超过300px后才停止
 if(parseInt(demo.style.left)<=300) requestAnimationFrame(arguments.callee);
});
</script>

示例二:

一直以来,JavaScript的动画都是通过定时器和间隔来实现的。虽然使用CSS transitions 和 animations使Web开发实现动画更加方便,但多年来以JavaScript为基础来实现动画却很少有所改变。直到Firefox 4的发布,才带来了第一种对JavaScript动画的改善的方法。但要充分认识改善,这有利于帮助我们了解web动画是如何演变改进的。
定时器Timer
用于创建动画的第一个模式是使用链式setTimeout()调用。在Netscape 3′s hayday的很长一段时期,开发者都记得一种在网络上随处可见的固定式最新行情状态栏,通常它类似于这样:

(function(){ 
 var msg = "新的广告", 
 len = 25, 
 pos = 0, 
 padding = msg.replace(/./g, " ").substr(0,len), 
 finalMsg = padding + msg; 
 function updateText(){ 
 var curMsg = finalMsg.substr(pos++, len); 
 window.status = curMsg; 
 if (pos == finalMsg.length){ pos = 0; } 
 setTimeout(updateText, 100); 
 } 
 setTimeout(updateText, 100); 
})();

 如果你想在浏览器中测试这段代码,你可以新建一个
标签用来模拟window.status,例如:newsticker example
这种让人烦恼的web模式,后来遭到对window.status禁用的抵抗,但随着Explorer 4和Netscape 4的发布,浏览器第一次给开发者更多对页面元素的控制权限,这种技术再次出现。这样就出现了使用javascript动态改变元素大小、位置、颜色等的一种全新动画模式。例如,下面就是一个将div宽度变化成100%的动画(类似于进度条):

(function(){ 
 function updateProgress(){ 
 var div = document.getElementByIdx_x("status"); 
 div.style.width = (parseInt(div.style.width, 10) + 5) + "%"; 
 if (div.style.width != "100%"){ setTimeout(updateProgress, 100); } 
 } 
 setTimeout(updateProgress, 100); 
})();

尽管动画在页面上的地方不同,但基本原理却是一样的:做出改变,用setTimeout()间隔使页面更新,然后setTimeout又执行下一次变化,这个过程反复执行,直到动画完成(见进度条动画),早期的状态栏动画是相同的技术,只是动画不一样而已。
间隔动画Intervals
随着成功将动画引入web,新的探索开始了。一个动画已经无法满足了,现在需要多个动画。首次尝试为每个动画创建多个动画循环,在早期的浏览器中使用setTimeout()来创建多个动画是有点复杂的,所以开发商开始使用setInterval()一创建单一的动画循环,来管理页面上所有的动画,一个使用wetInterval()的基本动画像这样:

(function(){ 
 function updateAnimations(){ 
 updateText(); 
 updateProgress(); 
 } 
 setInterval(updateAnimations, 100); 
})();

创建一个小动画库,updateAnimations()方法将每一个动画(同时看到一个新闻股票和进度条在一起运行)循环执行并进行适当的改变。如果没有动画需要更新,该方法可以退出而不做任何事情,甚至停止动画循环,直到有更多的动画更新做好准备。
动画问题比较棘手的问题是延迟应该为多少。间隔一方面必须足够短,从而使不同的动画都能流畅的进行,别一方面还要足够长,使得浏览器可以完成渲染。大多数浏览器的刷新频率为60HZ,即每秒60次刷新,大多数浏览器的刷新频率都不会比这个更频繁,因为他们知道,最终用户是得不到更好的体验的。
鉴于此,为流畅动画的最佳时间间隔为1000毫秒/ 60,约17ms。在这个频率你会看到流畅的动画,那是因为你最大的接近了浏览器能达到的频率。跟以前的动画相比,你会发现17ms间隔的动画更加平滑,也更快(因为动画更新更频繁,没有做其他任何修改的情况下),多个动画可能需要节流,以免17ms的动画完成得太快。
问题
即使使用setInterval()为基础的动画循环比多套使用setTimeout()的动画循环高效,这里还是存在问题。无论是setInterval()还是setTimeout()都无法达到精确,这个延迟即你指定的第二个参数仅仅表示何时代码会添加到浏览器的可能被执行的UI线程队列中。如果队列中有其他工作在此之前,那代码将会等到他完成才会执行。简而言之,毫秒级的延迟不是表示何时代码会执行,而是表示何时代码会添加进队列。如果UI线程处于繁忙状态或在处理用户动作,那么代码将不会被马上执行。
平滑动画的关键是理解下一帧何时被执行,直到现在都没有一个方法来保证下一帧将会在浏览器中被绘制。随着的日益流行和新的基于浏览器的游戏的出现,开发商对setInterval()和setTimeout()的不精准越来越感到失望。
浏览器的计时器分辨率加剧了这个问题,计时器对毫秒不精准,这里有一些常见的计时器分辨率:
Internet Explorer 8 and earlier 15.625ms
Internet Explorer 9 and later 4ms.
Firefox and Safari ~10ms.
Chrome has a timer 4ms.

IE在版本9之前的的分辨率为15.625,所以0~15之间的任意值可能是0或15,但没有分别。IE9的计时器分辨率改进为4ms,但涉及到动画时也是不具体的,chrome的计时器分辨率为4ms,firefox 和 safari的为10ms。因此即使你把间隔设定为最佳的显示效果,你也仅仅是得到这个近似值。
mozRequestAnimationFrame
Mozilla 的 Robert O'Callahan 在思考这个问题,并想出了一个独特的方案。他指出CSS transitions 和 animations的优势在于浏览器知道哪些动画将会发生,所以得到正确的间隔来刷新UI。而javascript动画,浏览器不知道动画正在发生。他的解决方案是创建一个mozRequestAnimationFrame()方法来告诉浏览器哪些javascript代码正在执行,这使得浏览在执行一些代码后得到优化。
mozRequestAnimationFrame()方法接受一个参数,是一个屏幕重绘前被调用的函数。这个函数用来对生成下合适的dom样式的改变,这些改变用在下一次重绘中。你可以像调用setTimeout()一样的方式链式调用mozRequestAnimationFrame(),例如:

function updateProgress(){ 
 var div = document.getElementByIdx_x("status"); 
 div.style.width = (parseInt(div.style.width, 10) + 5) + "%"; 
 if (div.style.left != "100%"){ 
 mozRequestAnimationFrame(updateProgress); 
 } 
}

mozRequestAnimationFrame(updateProgress); 
由于mozRequestAnimationFrame()只运行给定的函数一次,你需要在下一次UI动画的时候再次调用它。你也需要相同的方法来管理何时停止调用。很酷,是非常流畅的动画增强的实例。
因此,mozRequestAnimationFrame()解决了浏览器不知道Javascript动画正在执行和不知道多少才是合适的间隔的问题,但对于不知道何时你的代码才被真正执行,也是由这个方案来解决的。
传递给mozRequestAnimationFrame()的函数实际是一个下一次重绘何时发生的的时间码(以毫秒为单位自1970年1月1日计算)。这是很重要的一点:mozRequestAnimationFrame()实际上列表出将要重绘的点并可以告诉你他们所处的时间。这样你就能够决定怎样更好的来调整你的动画。
为了得到上次重绘过去的时间,你可以查询mozAnimationStartTime,其中包含了过去重绘的时间代码。减去传递回调时的这个值可以计算出下一次重绘到屏幕时所用的时间。使用这些值的典型模式如下:

function draw(timestamp){ 
 //calculate difference since last repaint 
 var diff = timestamp - startTime; 
 //use diff to determine correct next step 
 //reset startTime to this repaint 
 startTime = timestamp; 
 //draw again 
 mozRequestAnimationFrame(draw); 
} 
var startTime = mozAnimationStartTime; 
mozRequestAnimationFrame(draw);

关键是第一次不是通过callback调用时,mozAnimationStartTime是到mozRequestAnimationFrame()经过的时间。如果是在回调函数中,mozAnimationStartTime是通过参数传递进来的时间代码平均值。
webkitRequestAnimationFrame
在很多人热忠于chrome时,随即创建了webkitRequestAnimationFrame()方法。这个版本与firefox的版本在两方面有着细微的差别。一方面,它不通过回调函数传递时间代码,你将无法知道下次重绘何时发生,另一方面,它添加了第二个可选参数来确定哪一个DOM元素发生改变。因此,如果你知道重绘发生在页面哪个部分的元素内,你可以限制重绘发生的区域。
应该不会感到惊讶,有没有相应的mozAnimationStartTime,因为如果没有下一个重绘的时间信息不是很有益。有,只是webkitCancelAnimationFrame()取消了之前计划的重绘。
如果你不需要精确的时间差异,你可以用下面的方式来创建一个用于Firefox4和chrome10+的动画:

function draw(timestamp){ 
 //calculate difference since last repaint 
 var drawStart = (timestamp || Date.now()), 
 diff = drawStart - startTime; 
 //use diff to determine correct next step 
 //reset startTime to this repaint 
 startTime = drawStart; 
 //draw again 
 requestAnimationFrame(draw); 
} 
var requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame, 
startTime = window.mozAnimationStartTime || Date.now(); 
requestAnimationFrame(draw);

这种模式使用可用的方法来创建以花费多少时间为理念的循环动画。Firefox使用时间代码信息是有用的,而Chrome默认为欠精准的时间对象。当用这种模式的时候,时间的差异给你一种多少时间过去了的想法,但不会告诉你Chrome的下一次重绘出现在何时。不过这比只有多少时间过去了的模糊概念要好些。
总结
mozRequestAnimationFrame()方法的介绍为推动Javascript 动画及web的历史发展有着非常重要的作用。如前所述,JavaScript动画的态几乎和JavaScript的初期一样。随着浏览器逐渐推出CSS transitions 和 animations,很高兴看到基于JavaScript的动画的关注,因为这些在基于的游戏领域将变得更重要和更与CUP联系紧密。知道Javascript何时尝试动画,允许浏览器做更多的优化处理,包括在tab处于后台或移动设备电量过低时停止进程。
该requestAnimationFrame()API现在正由W3C起草一个新议案,并正由Mozilla和Google努力使之成为Web大舞台的一部分。很高兴能看到这两大集团这么迅速的兼容(可能不完全)实现。
RequestAnimFrame使用
对于一个侦中对DOM的所有操作,只进行一次Layout和Paint。
如果发生动画的元素被隐藏了,那么就不再去Paint。

window.requestAnimFrame = (function(){ 
 return window.requestAnimationFrame || 
 window.webkitRequestAnimationFrame || 
 window.mozRequestAnimationFrame || 
 window.oRequestAnimationFrame || 
 window.msRequestAnimationFrame || 
 function( callback ){ 
  window.setTimeout(callback, 1000/60); 
 }; 
})(); 
//调用 
function animationLoop(elem){ 
 requestAnimFrame(animationLoop); 
//logic 
} 
Or
window.requestAnimFrame = (function(w, r) { 
 w['r'+r] = w['r'+r] || w['webkitR'+r] || w['mozR'+r] || w['msR'+r] || w['oR'+r] || function(c){ w.setTimeout(c, 1000 / 60); }; 
 return w['r'+r]; 
})(window, 'equestAnimationFrame');

以上通过两段代码示例详解说明了使用requestAnimationFrame实现js动画性能好,希望大家喜欢。

Javascript 相关文章推荐
jQuery解析json格式数据简单实例
Jan 22 Javascript
深入浅析JavaScript中的Function类型
Jul 09 Javascript
js oncontextmenu事件使用详解
Mar 25 Javascript
JS实现加载时锁定HTML页面元素的方法
Jun 24 Javascript
详解vue.js之绑定class和style的示例代码
Aug 24 Javascript
vue实现搜索功能
May 28 Javascript
JS开发 富文本编辑器TinyMCE详解
Jul 19 Javascript
layer.js之回调销毁对话框的例子
Sep 11 Javascript
vue.js循环radio的实例
Nov 07 Javascript
vue-element-admin 菜单标签失效的解决方式
Nov 12 Javascript
微信小程序获取公众号文章列表及显示文章的示例代码
Mar 10 Javascript
vuecli3.x中轻松4步带你使用tinymce的步骤
Jun 25 Javascript
JavaScript实现的圆形浮动标签云效果实例
Aug 06 #Javascript
javascript跨域方法、原理以及出现问题解决方法(详解)
Aug 06 #Javascript
jquery实现选中单选按钮下拉伸缩效果
Aug 06 #Javascript
js实现网页抽奖实例
Aug 05 #Javascript
javascript实现多栏闭合展开式广告位菜单效果实例
Aug 05 #Javascript
JavaScript实现可拖拽的拖动层Div实例
Aug 05 #Javascript
JQuery悬停控制图片轮播——代码简单
Aug 05 #Javascript
You might like
UCenter Home二次开发指南
2009/05/28 PHP
PHP整数取余返回负数的相关解决方法
2014/05/15 PHP
Zend Framework教程之Zend_Form组件实现表单提交并显示错误提示的方法
2016/03/21 PHP
tagName的使用,留一笔
2006/06/26 Javascript
Ext JS 4实现带week(星期)的日期选择控件(实战二)
2013/08/21 Javascript
Javascript封装DOMContentLoaded事件实例
2014/06/12 Javascript
Javascript实现多彩雪花从天降散落效果的方法
2015/02/02 Javascript
js实现内容显示并使用json传输数据
2016/03/16 Javascript
EXT中单击button按钮grid添加一行(光标位置可设置)的实例代码
2016/06/02 Javascript
用js读写cookie的简单方法(推荐)
2016/08/08 Javascript
JavaScript 控制字体大小设置的方法
2016/11/23 Javascript
js实现图片切换(动画版)
2016/12/25 Javascript
微信小程序 解决请求服务器手机预览请求不到数据的方法
2017/01/04 Javascript
详解angularJs中自定义directive的数据交互
2017/01/13 Javascript
Zepto实现密码的隐藏/显示
2017/04/07 Javascript
解决使用layui对select append元素无效或者未及时更新的问题
2019/09/18 Javascript
京东优选小程序的实现代码示例
2020/02/25 Javascript
python绘图库Matplotlib的安装
2014/07/03 Python
python正常时间和unix时间戳相互转换的方法
2015/04/23 Python
python基于右递归解决八皇后问题的方法
2015/05/25 Python
Python中的条件判断语句与循环语句用法小结
2016/03/21 Python
Python 使用os.remove删除文件夹时报错的解决方法
2017/01/13 Python
解决python大批量读写.doc文件的问题
2018/05/08 Python
numpy按列连接两个维数不同的数组方式
2019/12/06 Python
python中reload重载实例用法
2020/12/15 Python
浅谈Selenium+Webdriver 常用的元素定位方式
2021/01/13 Python
CSS3动画效果回调处理详解
2014/12/10 HTML / CSS
纯css3使用vw和vh实现自适应的方法
2018/02/09 HTML / CSS
英国领先的葡萄酒专家:Majestic Wine
2017/05/30 全球购物
波兰香水和化妆品购物网站:Notino.pl
2017/11/07 全球购物
是什么让J2EE适合用来开发多层的分布式的应用
2015/01/16 面试题
幼儿园保育员辞职信
2014/01/12 职场文书
大学生咖啡店创业计划书
2014/01/21 职场文书
先进集体事迹材料
2014/02/17 职场文书
公司财务会计主管应聘求职信
2014/09/26 职场文书
从QQtabBar看css命名规范BEM的详细介绍
2021/08/07 HTML / CSS