详细解析setTimeout和setInterval以及事件机制

一道面试题引发的文章:

用setTimeout实现setInterval

var mySetInterval = function(func, duration){
    function interval(){
        setTimeout(interval, duration);
        func()
    }
    setTimeout(interval, duration);
}

为什么要用setTimeout实现setInterval?

在日常编码中setTimeout和setInterval的延迟时间经常不准确, 这是因为二者不是立即执行的, JS是单线程, 有一个事件队列机制

setTimeout是延迟delay毫秒后, 将回调函数加入事件队列, 事件什么时候执行到此处不一定, 所以会有延迟;如果delay为0, 就代表立刻插入到事件队列

setInterval是延迟delay毫秒后, 看看事件队列中是否存在还没有执行的回调函数, 如果还存在, 就不要再往事件队列中添加回调函数了.

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

因此setTimeout是确定可以向事件队列添加事件的, 但是setInterval不确定.

一道经典的面试题, 输出是什么

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

众所周知, 答案是在1s后同时输出5个5;
因为for循环先执行完(宏任务, 不知道的去 事件循环机制), i = 5; 1s后5个setTimeout同时被添加宏任务队列,由于没有其他的宏任务和微任务, 此时, 5个5被同时打印出来.
比如:

var testSetTimeout = function(){
    for(var i = 0;i < 5;i++){
        setTimeout(() => {
            console.log(i);
        }, 1000);
    }
    console.log(11111);
}
testSetTimeout()

就会先打印出11111, 一秒后打印出5个5

若想依次打印出0,1,2,3,4的解决办法呢?

依次打印出0,1,2,3,4的解决办法

解决办法一: 立即执行函数
for (var i = 0; i < 5; i++) {
  (function(i){   //立即执行函数
    setTimeout(function (){
      console.log(i);
     },1000);
  })(i);
}

那你有没有想过, 为什么立即执行函数中的setTimeout也是一秒后统一发送到事件队列中,为什么它就可以获取到每次循环的变量呢? 这是因为立即执行函数会创建一个独立的作用域,来保存每次循环的变量, setTimeout中的函数被调用时, 会去找自己的作用域链, 因此才能获取到循环当次的变量.

第二种方法, let

很多人都知道, 最简单的方法就是将var改为let

 for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

那原因是什么呢? 下一个问题

在for循环中var和let的作用域究竟是什么样子的?

在for循环中, 执行顺序是这样的
设置循环变量(var i = 0) -> 循环判断(i<3) -> 满足执行循环体 -> 循环变量自增(i++)
所以一个循环可以被解析成下面这种格式(以3个为例), 最后调用函数的时候, i的值统一已经变成3了

{
  //我是父作用域
  var i = 0;
  if (0 < 3) {
    setTimeout(function(){
        console.log(i)
    },1000)
  };
  i++; //为1
  if (1 < 3) {
    setTimeout(function(){
        console.log(i)
    },1000)
  };
  i++; //为2
  if (2 < 3) {
    setTimeout(function(){
        console.log(i)
    },1000)
  };
  i++; //为3
  // 跳出循环
}

但是如果将上面代码的var改成let,也是只声明一次, 为什么就相互独立了呢?
这篇博客讲的很深入 有兴趣可以看看

简单说, 就是ES6中, let也只是被声明了一次, 但是每次进入循环内, 都相当于进入了一个新的作用域. 函数会拷贝父作用域到[[scoped]]属性上, 此时由于i是基本数据类型, 所以被直接拷贝进来, 每次函数被执行时, 会将每个函数的[[scoped]]属性放到当前执行环境的作用域链上, 因此输出的是不同的值
如果将let i 中的i变成对象 let i = {y: 1}, 就会发现, 输出的不是依次为0,1,2,3,4 了

for(let y = {i: 0};y.i < 5;y.i++){
        setTimeout(() => {
            console.log(y.i);
        }, 1000);
    }

这样, 输出的就是1s后一次性输出5个5了, 因为y={i:0} 是对象, 每次传进函数作用域的都是一个对象的引用, 因此最后取的都是同一个对象.

那么, 立即执行函数呢? 是不是也是这样, 因此, 笔者又去试了下用立即执行函数的方法. 你猜猜结果是什么?

for(var y = {i: 0};y.i < 5;y.i++){
        (function(y){
            setTimeout(() => {
                console.log(y.i);
            }, 1000);
        })(y)
    }

哈哈哈, 果不其然, 也是一秒后一次性输出5个5

如果传给立即执行函数的参数是i

for(var y = {i: 0};y.i < 5;y.i++){
        (function(i){
            setTimeout(() => {
                console.log(i);
            }, 1000);
        })(y.i)   
    }

这个结果就是1s后输出0,1,2,3,4
你懂了吗?

最后

感谢你能看到这, 我知道向上爬的人很多, 但是愿意向下扎根的人很少.
如果你能看到这, 祝福你~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值