循环中的定时器问题

前言

通过定时器循环输出不同的值

问题

一提到循环,理所当然的就想到了for循环

比如下面的代码

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

怎么才能在每次输出i之后再执行循环呢,因为for循环执行完毕后,才执行定时器,所以结果输出了3个3

定时器只是定时开启一个新的线程,不会暂停for循环

原因

JS众所周知是单线程的,可能有人会认为在上面的例子中会先阻塞等待定时器执行完成后再执行后面的循环

为了解决单线程的缺陷,引入了异步机制

异步机制主要是利用一个我们平时很少去关注的一个知识点-浏览器的多线程

可以参考博客第三小节浏览器的多线程

所以对于上面连续打印3个3的问题,由于变量 i 直接暴露在全局作用域内,当调用 console.log 函数开始输出时,这是循环已经结束

方法

最简单的,不能用for循环,那就用函数递归

(function f(i) {
     console.log(i)
    if (++i<3)
        setTimeout(function(){f(i)}, 1000)
})(0)

就可以依次输出出0,1,2

解决这种setTimeout变量控制的方法有多种,早在stackoverflow上就有大神解释了,可以参考

大体方法有三种:

第一种是为每个定时器处理函数创建不同的“i”变量副本

function doSetTimeout(i) {
  setTimeout(function() { console.log(i) }, 1000)
}

for (var i = 0; i < 3; i++)
  doSetTimeout(i)

这里通过定义一个函数来实现中介的作用,从而创建了变量的值的副本

由于setTimeout()是在该副本的上下文中调用的,所以它每次都有自己的私有的"i"以供使用

但该方法也有它的缺陷,像上述代码块几乎是同时输出了0,1,2三个值

因为设立一些时间间隔相同的连续的setTimeout()将导致所有延时处理程序同时被调用,设置timer(对setTimeout()的调用)几乎不消耗时间

也就是说,告诉系统“请在1000毫秒后调用此函数”将会被立即返回,因为在timer队列中安装延时请求的过程非常快

可以修改为

function doSetTimeout(i,j) {
    setTimeout(function() { console.log(i) }, j*1000)
}
for (var i = 0,j=1; i < 3; i++,j++)
doSetTimeout(i,j)

第二种方法是使用bind方法

for (var i = 0, j = 1; i < 3; i++, j++) {
    setTimeout(function() {
        console.log(this)
    }.bind(i), j * 1000)
}

该方法允许显式地指定函数调用时 this 所指向的值

bind()的作用类似call和apply,都是修改this指向

但是call和apply是修改this指向后函数会立即执行,失去了定时器的作用

而bind则是返回一个新的函数,会创建一个与原来函数主体相同的新函数,新函数中的this指向传入的对象

第三种方法是使用立即执行函数给setTimeout创建一个闭包

类似于上面提到的函数递归方法

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

因为 Javascript 只有两种作用域,一是全局作用域,二是函数作用域,它是没有块级作用域的

所以闭包的出现就相当于利用一个匿名函数的壳模拟出一个块级作用域

上述代码块往匿名函数内部传的参数将会被拷贝一份,也就是说循环没执行一次就拷贝变量 i 的值到匿名函数内部

还可以使用es6中的let变量,也是利用的块级作用域原理

for (let i =0,j=1; i < 3; i++,j++) {   
   setTimeout(function() {
       console.log(i)
   }, j*1000)
}