ホームページ > 記事 > ウェブフロントエンド > requestAnimationFrame を使用して、優れた JS アニメーション パフォーマンス_JavaScript スキルを達成する
requestAnimationFrame を使用して、優れたパフォーマンスの js アニメーションを実現します。まず、setTimeout や setInterval と比較した requestAnimationFrame の利点について簡単に説明します。
例 1:
requestAnimationFrame には、setTimeout や setInterval に比べて 2 つの主な利点があります。
1. requestAnimationFrame は、すべての DOM 操作を各フレームに集中させ、1 回の再描画またはリフローで完了します。再描画またはリフローの時間間隔は、一般的に、1 秒あたり 60 フレームです。 。
2. 非表示または非表示の要素では、requestAnimationFrame は再描画またはリフローされません。これは、当然、CPU、GPU、およびメモリの使用量が少なくなります。
setTimeout や setInterval と同様、requestAnimationFrame はグローバル関数です。 requestAnimationFrame を呼び出した後、ブラウザーは自身の頻度に従って再描画するように要求し、ブラウザーが再描画しようとすると、この関数が呼び出され、そのコールバックがこの関数に渡されます。関数は時間をパラメータとして受け取ります。 requestAnimationFrame の関数は 1 回限りであるため、アニメーション効果を実現したい場合は、setTimeout を使用してアニメーションを実装するのと同じように、requestAnimationFrame を継続的に呼び出す必要があります。 requestAnimationFrame 関数はリソース識別子を返します。この識別子をパラメータとして cancelAnimationFrame 関数に渡して、requestAnimationFrame コールバックをキャンセルできます。 setTimeoutのclearTimeoutとよく似ていますか?
したがって、requestAnimationFrame はアニメーション用に特別に調整された setTimeout のパフォーマンスが最適化されたバージョンであると言えます。違いは、requestAnimationFrame がコールバック関数自体の実行時間を指定せず、ブラウザーの組み込みリフレッシュに従ってコールバックを実行することです。もちろん、ブラウザが実現できる最高のアニメーション効果を実現できます。
現時点では、requestAnimationFrame をサポートするブラウザーの一部には独自のプライベート実装が残っているため、requestAnimationFrame をサポートしないブラウザーの場合は、setTimeout のみを使用できます。これは、2 つの使い方がほぼ同じであるためです。 2つは互換性があります。それは難しいことではありません。 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 デモを持つ 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>
例 2:
JavaScript アニメーションは常にタイマーとインターバルを通じて実装されてきました。 CSS トランジションとアニメーションの使用により、Web 開発におけるアニメーションはより便利になりましたが、JavaScript に基づくアニメーションの実装は長年にわたってほとんど変わっていません。 JavaScript アニメーションに対する最初の改良点が導入されたのは、Firefox 4 のリリースでした。しかし、改善を完全に理解するには、Web アニメーションがどのように進化し、改善されているかを理解するのに役立ちます。
タイマー
アニメーションを作成する最初のパターンは、連鎖した setTimeout() 呼び出しを使用することです。 Netscape 3 の全盛期の長い間、開発者は、インターネット上のどこでも見られる、ある種の固定された最新の引用ステータス バーを覚えていました。それは通常、次のようなものでした。
(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动画性能好,希望大家喜欢。