使用 JavaScript 做动画时,或者做与时间有关的事情,你有三个选择:setTimeout
, setInterval
和 requestAnimationFrame
,这三个函数有点类似,也有点区别,也有一些坑等你来踩。
setTimeout(code, millseconds)
用于延时执行参数指定的代码,如果在指定的延迟时间之前,你想取消这个执行,那么直接用clearTimeout(timeoutId)
来清除任务,timeoutID
是 setTimeout
时返回的;setInterval(code, millseconds)
用于每隔一段时间执行指定的代码,永无停歇,除非你反悔了,想清除它,可以使用 clearInterval(intervalId)
,这样从调用 clearInterval 开始,就不会在有重复执行的任务,intervalId
是 setInterval
时返回的;requestAnimationFrame(code)
,一般用于动画,与 setTimeout
方法类似,区别是 setTimeout
是用户指定的,而 requestAnimationFrame
是浏览器刷新频率决定的,一般遵循 W3C 标准,它在浏览器每次刷新页面之前执行。这是一个误区,不要以为设置 setTimeout
会让你的程序停下来,它只会让它包裹的代码延迟指定的时间执行,在延迟的时间内,后面的代码还是会执行的!举个例子,你猜结果是啥?
var someVar = 'init-value';
setTimeout(function () {
console.log('The first output at ' + (new Date()).getSeconds() + '-th second content : ' + someVar);
}, 2000);
console.log('The second output at ' + (new Date()).getSeconds() + '-th second content : ' + someVar);
someVar = 'new-value';
如果 setTimeout
真的能让程序暂停的话,那么输出结果应该是:
The first output at x-th second content : new-value
The second output at x-th second content : init-value
然而并不是,实际的输出结果:
为了解释其中的原因,来个示意图看看:
看这个图应该很好理解,注意“如果线程空闲的话”这句话,这句话的重要程度足以让你在心中默念三遍,这就是“JavaScript定时器不准确”的原因,因为如果线程不空闲,那么 setTimeout
会等到线程空闲才会执行,等多长时间还真不好说。所以,注意这一点!!!
有一个重要的点,在浏览器执行环境中,它们三个都是 window
对象的方法,而我们也知道,函数中的 this
对象指向调用函数的对象,那么他们的第一个参数中用到的 this
都将指向 window
,这个时候如果你开发的是一个类组件,你就等着受折磨吧,还是举个例子。
function Test () {
this.name = "庆祝亚运会";
}
Test.prototype.getName = function () {
console.log(this.name);
};
Test.prototype.testInterval = function () {
var interval = setInterval(this.getName, 1000);
};
var test = new Test();
test.testInterval();
你们猜会输出什么?Look :
当你看到这每隔一秒输出的一个 undefined
,是不是当时是拒绝的,反正我就果断按下了 crtl + c
了解了它。那么哪里的问题呢?就是作用域的问题, setInterval
或者 setTimeout
指定的代码中的 this
对象指向的是 window
,而全局作用域中并没有 name
这个变量,所以就是 undefined
捞。
为了证实我们的解释,上面的输出是在 node 中测试的,我们切换到浏览器环境,因为在浏览器中才有全局对象 window
,node 中都是一个个的模块。如下:
<html>
<head>
<meta charset="utf-8">
<title>测试title>
head>
<body>
<script type="text/javascript">
window.name = "global name";
function Test () {
this.name = "庆祝亚运会";
}
Test.prototype.getName = function () {
console.log(this.name);
};
Test.prototype.testInterval = function () {
var interval = setInterval(this.getName, 1000);
};
var test = new Test();
test.testInterval();
script>
body>
html>
输出如下,就不解释了,有图有真相。
问题原因是找到了,但是咋个解决?
众所周知,我们可以使用 Function
对象的原型方法call
,apply
, bind
来解决函数作用域的问题,call(this [, arg1, arg2 ...])
和 apply(this [argArr])
是一样的,都是立即在指定的 this
环境下执行函数,只是参数指定的方式不一样。而 bind
不一样,它将一直绑定在指定的作用域中,之后的每次调用都会使用这个作用域。
利用bind
,上面的这个问题就可以解决了,看到下面的结果,你就知道 bind
和 call/apply
的区别了,你也就明白为什么 w3c 非要给 Function
加个和它们类似的原型方法了:
Test.prototype.testInterval = function () {
var interval = setInterval(this.getName.bind(this), 1000);
};
这个坑其实并不能算作坑,它并不能让你陷入错误的沼泽,但是会稍稍拖慢你的应用性能,原因和 eval
方法一致,把第一个参数指定为字符串,函数首先得把它翻译成可执行的代码,SO,尽量不要这么做!
从上面基本用法的描述中,我们可以得出在做动画时,requestAnimationFrame
肯定比 setTimeout
更优的结论,但是 requestAnimationFrame
毕竟是新方法,虽然得到了很多的支持:
但是,MDN 在讲述这部分时,特意提出了警告
W3中明确提示:Web性能工作室,是不打算继续维护这个方法的(因此是否继续使用这个方法,请关注后续的W3发布新闻)
So,该怎么做,最好在使用 requestAnimationFrame
时注意进行降级,不支持时使用 setTimeout
来个 polyfill
。
本文主要讲了 setTimeout
,setInterval
和 requestAnimationFrame
的使用和一些很容易遇到的坑,可能不全,有没有考虑到的,请各位在评论里提个醒!