setTimeout和setInterval
如果想使用JavaScript自定义动画效果,通常我们会使用setTimeout和setInterval,它们都是全局的方法。这两个函数的参数是一样的,第一个是回调函数(JavaScript代码的字符串也行,但是不推荐),第二个是以毫秒为单位的时间间隔。虽然参数一样,但是它们的行为还是有很大不同。
setTimeout( someFun, 1000 )表示在定义之后的一秒钟后会调用someFun这个函数;setInterval( someFun, 1000 )则是在定义之后,每隔一秒钟都会调用someFun。这些都是理想状态下,在实际情况中,定时器可能并不会准确的在你预设的时间后执行回调函数。原因是JavaScript是单线程的,因此,即使你定义了一个定时器,定时器中的回调函数也必须等待它之前的任务完成之后,它才会被触发。看下面的例子,setTimeout虽然指定了在1秒后执行someFun,但是它之后的计算任务耗时超过2秒,所以someFun必须等计算的任务完成后才能被触发,所以并不是理想中的1秒钟后救执行。如果换成setInterval的话,情况就变成了在计算任务完成之后,someFun会连续被触发两次,之后就会每隔一秒触发一次。原因是setInterval的机制是从定义开始每隔一秒种就往JS任务队列中插入执行回调函数的任务,由于计算任务耗时超过两秒,所以就会在执行计算任务的过程中,在任务队列后面插入了两次执行回调函数的任务。当计算任务一旦完成,线程就立刻调用等待的任务,所以就连续触发了两次,之后由于JavaScript线程空闲,就能比较准确的以一秒的间隔触发了。
例子:
var time1;
var count = 0;
function someFun() {
alert( 'Time:' + ( new Date().getTime() - time1 ) / 1000 + 's' );
}
function init() {
setTimeout( someFun, 1000 );
time1 = new Date().getTime();
var dt1 = new Date().getTime();
for ( var i = 1; i <= 1000000; i++) {
count ++;
}
var dt2 = new Date().getTime();
console.log( 'Compute:' + ( dt2 - dt1 ) / 1000 + 's');
}
init();
这两个函数都会返回一个ID,用于标示这个定时器。如果我们想取消定时器了,我们就可以使用者ID。取消定时器的相应方法是clearTimeout和clearInterval,只需要将返回的那个ID传入,就可以取消指定的定时器了。
setInterval可以连续的触发回调函数,setTimeout只能触发一次,但是我们可以用setTimeout来模拟连续触发。看下面的例子:
var lastUpdateTime = new Date().getTime();
function doAnimation() {
// Code... 游戏引擎执行更新
var updateAfter = new Date().getTime();
var useTime = updateAter - lastUpdateTime;
setTimeout( doAnimation, ( 1000 / 60 ) - useTime );
lastUpdateTime = new Date().getTime();
}
doAnimation();
每次调用doAnimation,都会定义了一个新的超时定时器,就会继续调用doAnimation,这样就能连续触发。竟然setInterval可以做到,为什么我们还需要用这种方式呢?事实上这种方式在实际开发中优于setInterval,因为setInterval完全交给了浏览器来控制,可能出现后面的任务先于前面的任务被触发。如果用setTimeout我们就可以控制什么时候触发回调函数,假设是游戏引擎,引擎计算一次更新的耗时不一定每次都一样,但是我们需要保持更新频率的稳定,就可以像上面那么做。
requtestAnimationFrame
为了解决setTimeout不准确问题和提高动画的流畅度,大部分浏览器厂商都实现了requestAnimationFrame方法,它的主要目的就是来实现动画。requestAnimationFrame只接受一个参数,就是回调函数。它不需要设置时间间隔,因为它会在浏览器每次刷新之前执行回调函数的任务。这样我们动画的更新就能和浏览器的刷新频率保持一直,从而大大提高动画的流畅度,使用GPU去控制什么时候更新,也可以减小CPU的压力。
在游戏引擎中,如果使用setTiemout方法,那么它就是使用CPU去计算更新的。有一点需要注意的是:CPU计算更新的任务完成后,并不代表这些更新就能立刻显示在浏览器上,因为还必须等待浏览器的下一次刷新,才能显示更新后的动画。因此,如果CPU的的更新频率和浏览的刷新频率存在误差,那么势必会影响动画的流畅性。因此对于游戏引擎中,动画部分的更新,可以采用该方法。下面给出一个使用它的例子:
<script type="text/javascript">
// 关键性代码,由于requestAnimationFrame没有统一的标准,所以基于浏览器不同而异,下面是兼容性的写法
var requestAnimationFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function ( callback ) {
window.setTimeout( callback, 1000 / 60 );
}
})();
var xspeed = 100, yspeed = 100;
var lastUpdateTime = new Date().getTime();
var x = 10, y = 10, w = 400, h = 200;
var cvs = document.getElementById( 'myCanvas' ); // 需要你的浏览器支持HTML5,在body中定义一个canvas标签,id为myCanvas即可
var ctx = cvs.getContext('2d');
cvs.width = w;
cvs.height = h;
ctx.fillStyle = 'rgb(234, 87, 23)';
ctx.beginPath();
ctx.arc( x, y, 10, 0, Math.PI * 2, true );
ctx.closePath();
ctx.fill();
function doAnimation() {
var currentTime = new Date().getTime();
var xdistance = ( currentTime - lastUpdateTime ) * xspeed / 1000;
var ydistance = ( currentTime - lastUpdateTime ) * yspeed / 1000;
x += xdistance ;
y += ydistance ;
if ( x + 10 >= w )
xspeed = -1 * Math.abs( xspeed );
else if ( x - 10 <= 0 )
xspeed = Math.abs( xspeed );
if ( y + 10 >= h )
yspeed = -1 * Math.abs( yspeed );
else if ( y - 10 <= 0 )
yspeed = Math.abs( yspeed );
ctx.clearRect( 0, 0, w, h );
ctx.beginPath();
ctx.arc( x, y, 10, 0, Math.PI * 2, true );
ctx.closePath();
ctx.fill();
lastUpdateTime = new Date().getTime();
requestAnimationFrame( doAnimation );
}
doAnimation();
</script>