HTML5之动画优化(requestAnimationFrame)
- 定时器setInterval实现的匀速动画为什么不是匀速?
- window.requestAnimationFrame()
一、定时器setInterval实现的匀速动画为什么不是匀速?
以上提问并非通过计算时间戳来计算每帧运动,而是直接使用定时器按照既定的间隔时间叠加每次运动距离的方式,这里主要意在解析浏览器中定时器与页面重绘导致的不准确性,先来看一个示例:
1 <style> 2 #a{ 3 width: 100px; 4 height: 100px; 5 background-color:#faf; 6 position: absolute; 7 } 8 </style> 9 <div id="a"></div> 10 <script> 11 var aDom = document.getElementById('a'); 12 function move(){ 13 aDom.style.left = aDom.offsetLeft + 100 + 'px'; 14 if(aDom.offsetLeft > 800){ 15 clearInterval(timer); 16 } 17 } 18 var timer = setInterval(move,10); 19 </script>
示例中定时器每隔10毫秒元素移动100px,从定时器的意义上来说这就是匀速运动了,但是定时器只能每隔10毫秒将每一次运动的执行函数添加执行队列中,执行并修改元素的属性值,但是实际呈现到页面的却是1秒钟刷新60次的重绘来完成的,这种时间差会产生什么问题呢?
如果不了解浏览器js线程的话,建议有必要先了解:浏览器UI多线程及JavaScript单线程运行机制的理解
继续来看下面的分析:
第一次运动点:100px —— 实际运动(渲染到页面)的时间:16.667毫秒 —— 实际修改元素属性值的时间10毫秒;
第二次运动点:300px —— 实际运动(渲染到页面)的时间:33.334毫秒 —— 实际修改元素属性值的时间20毫秒(200px),实际修改元素属性值的时间30毫秒(300px);
第三次运动点:500px —— 实际运动(渲染到页面)的时间:50.001毫秒 —— 实际修改元素属性值的时间40毫秒(400px),实际修改元素属性值的时间50毫秒(500px);
第四次运动点:600px —— 实际运动(渲染到页面)的时间:66.668毫秒 —— 实际修改元素属性值的时间60毫秒(600px);
第五次运动点:800px —— 实际运动(渲染到页面)的时间:83.335毫秒 —— 实际修改元素属性值的时间70毫秒(700px),实际修改元素属性值的时间80毫秒(800px);
通过上面的分析,原本以定时器的执行逻辑,应该运动8次匀速到达终点的动画,实际上使用了5次非匀速的方式到达终点。
基于以上的原因我们在实现动画的时候采用的是执行时间比值的方式来实现元素运动:
1 var h = (new Date()).getTime(); //记录初始时间戳 2 var ratio = null; 3 var speed = null; 4 var t = setInterval(function(){ 5 var performH = (new Date()).getTime(); //获取当前时间戳 6 ratio = (performH -h) / 80; //当前所用时间 / 动画执行实行 7 speed = ratio * 800; //使用时间比值计算得出当前元素运动位置 8 if(ratio < 1){ 9 aDom.style.left = speed + "px"; 10 }else{ 11 aDom.style.left = 800 + "px"; 12 clearInterval(t); 13 } 14 },1000/60);
这种处理方式理论上最接近匀速运动,但是还是会受js执行栈与浏览器重绘渲染速率的影响,在HTML5中提供window.requestAnimationFrame()这个API来解决这个问题,既然是HTML5就必然会有兼容性问题,下一节来具体分析。
二、window.requestAnimationFrame()
MDN手册:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame#Notes
window.requestAnimationFrame()告诉浏览器希望执行一次动画,并要求浏览器在下次重绘之前调用指定的回调函数更新动画。
//语法 window.requestAnimationFrame(callback);
callback(回调函数):浏览器下次重绘前执行,callback执行时window.requestAnimationFrame()内部会给这个回调函数传入DOMHighResTimeStamp
参数,DOMHighResTimeStamp参数指示当前被requesAnimationFrame排序的回调函数被触发时间。可以理解为时间戳,以ms(毫秒)为单位。
1 //上面的示例基于requestAnimationFrame实现 2 var startA = null; 3 function a(timestamp){ 4 if(!startA) startA = timestamp; 5 var progress = timestamp - startA; 6 console.log(progress); 7 aDom.style.left = Math.min(progress / 80 * 800, 800) + 'px'; 8 if(progress < 80){ 9 window.requestAnimationFrame(a); 10 } 11 } 12 window.requestAnimationFrame(a);
window.requestAnimationFrame()是HTML5的API就必然会有兼容性问题:
封装兼容性的window.requestAnimationFrame():
1 window.requestAnimFrame = (function(){ 2 return window.requestAnimationFrame || 3 window.webkitRequestAnimationFrame || 4 window.mozRequestAnimationFrame || 5 function(callback){ 6 window.setTimeout(callback, 1000/60); 7 } 8 })();
即使使用setInterval()来实现兼容,但并不代表其具备requestAnimationFrame()精准性。window.requestAnimationFrame()执行后会返回一个long整数,请求ID,是回调列表中唯一的标识。非零值,可以使用这个值来给window.cancelAnimationFrame()取消回调函数。
兼容window.cancelAnimationFrame():
1 window.cancelAnimFrame = (function(){ 2 return window.cancelAnimationFrame || 3 window.webkitCancelAnimationFrame || 4 window.mozCancelAnimationFrame || 5 function(id){ 6 window.clearTimeout(id); 7 } 8 })();