setTimeout, setInterval 与 requestAnimationFrame 隐藏的各种坑

使用 JavaScript 做动画时,或者做与时间有关的事情,你有三个选择:setTimeout, setIntervalrequestAnimationFrame ,这三个函数有点类似,也有点区别,也有一些坑等你来踩。

基本用法与区别


  • setTimeout(code, millseconds) 用于延时执行参数指定的代码,如果在指定的延迟时间之前,你想取消这个执行,那么直接用clearTimeout(timeoutId)来清除任务,timeoutIDsetTimeout 时返回的;
  • setInterval(code, millseconds)用于每隔一段时间执行指定的代码,永无停歇,除非你反悔了,想清除它,可以使用 clearInterval(intervalId),这样从调用 clearInterval 开始,就不会在有重复执行的任务,intervalIdsetInterval 时返回的;
  • requestAnimationFrame(code),一般用于动画,与 setTimeout 方法类似,区别是 setTimeout 是用户指定的,而 requestAnimationFrame 是浏览器刷新频率决定的,一般遵循 W3C 标准,它在浏览器每次刷新页面之前执行。

省省劲儿,setTimeout 不能让你的程序暂停


这是一个误区,不要以为设置 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

然而并不是,实际的输出结果:

实际输出

为了解释其中的原因,来个示意图看看:

setTimeout, setInterval 与 requestAnimationFrame 隐藏的各种坑_第1张图片

看这个图应该很好理解,注意“如果线程空闲的话”这句话,这句话的重要程度足以让你在心中默念三遍,这就是“JavaScript定时器不准确”的原因,因为如果线程不空闲,那么 setTimeout 会等到线程空闲才会执行,等多长时间还真不好说。所以,注意这一点!!!

小心它们的作用域(this)


有一个重要的点,在浏览器执行环境中,它们三个都是 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 :

setTimeout, setInterval 与 requestAnimationFrame 隐藏的各种坑_第2张图片

当你看到这每隔一秒输出的一个 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>

输出如下,就不解释了,有图有真相。

setTimeout, setInterval 与 requestAnimationFrame 隐藏的各种坑_第3张图片

call, apply, bind 来解围


问题原因是找到了,但是咋个解决?

众所周知,我们可以使用 Function 对象的原型方法call,apply, bind 来解决函数作用域的问题,call(this [, arg1, arg2 ...])apply(this [argArr]) 是一样的,都是立即在指定的 this 环境下执行函数,只是参数指定的方式不一样。而 bind 不一样,它将一直绑定在指定的作用域中,之后的每次调用都会使用这个作用域。

利用bind,上面的这个问题就可以解决了,看到下面的结果,你就知道 bindcall/apply 的区别了,你也就明白为什么 w3c 非要给 Function 加个和它们类似的原型方法了:

Test.prototype.testInterval = function () {
     
  var interval = setInterval(this.getName.bind(this), 1000);
};

setTimeout, setInterval 与 requestAnimationFrame 隐藏的各种坑_第4张图片

尽量别把第一个参数写成字符串


这个坑其实并不能算作坑,它并不能让你陷入错误的沼泽,但是会稍稍拖慢你的应用性能,原因和 eval 方法一致,把第一个参数指定为字符串,函数首先得把它翻译成可执行的代码,SO,尽量不要这么做!

动画中 setTimeout 和 requestAnimationFrame 的权衡


从上面基本用法的描述中,我们可以得出在做动画时,requestAnimationFrame 肯定比 setTimeout 更优的结论,但是 requestAnimationFrame 毕竟是新方法,虽然得到了很多的支持:

setTimeout, setInterval 与 requestAnimationFrame 隐藏的各种坑_第5张图片

但是,MDN 在讲述这部分时,特意提出了警告

W3中明确提示:Web性能工作室,是不打算继续维护这个方法的(因此是否继续使用这个方法,请关注后续的W3发布新闻)

So,该怎么做,最好在使用 requestAnimationFrame 时注意进行降级,不支持时使用 setTimeout 来个 polyfill

总结


本文主要讲了 setTimeoutsetIntervalrequestAnimationFrame 的使用和一些很容易遇到的坑,可能不全,有没有考虑到的,请各位在评论里提个醒!

你可能感兴趣的:(Web前端,setTimeout,js动画,js程序暂停,bind,js定时器)