问题描述:

想要在js中用setTimeout实现这么一个功能:每隔一秒输出一个数字。我们的js代码大概是这样的:

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

运行这段代码会发现,程序在1秒后输出了3个3。(不但没有每隔一秒输出,而且输出的数字还全都是3)

原因分析:

这跟js的阻塞机制有关。
js阻塞机制,跟js引擎的单线程处理方式有关,每个window一个js线程。所谓单线程,即在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码。
由于浏览器是事件驱动的(Event driven),因此浏览器中很多行为是异步(Asynchronized)的,很容易有事件被同时或者连续触发。当异步事件发生时,会创建事件并放入执行队列中,等待当前代码执行完成之后再执行这些代码,如鼠标点击事件发生、定时器触发事件发生、XMLHttpRequest完成回调这些事件,都会被放入执行队列中等待。

因此在上述的代码中,setTimeout这个定时器需要等待for循环执行完成,而for循环执行完成了之后,i已经是3了,此时才开始执行setTimeout,因此console.log(i)会是3。

我以为的程序执行过程:

javascript实现休眠 js 休眠_for循环


实际的程序执行过程:

javascript实现休眠 js 休眠_javascript_02

解决方案:

方案1

在setTimeout外部包装一个立即执行的匿名函数,将for循环的i作为参数传入进去,使得延迟函数的回调可以将新的作用域封闭在每次迭代的内部。

for (var i = 0; i < 3; i++) {
    (function (time, data) {   // 匿名函数的形参
        setTimeout(function () {
            console.log(`这是第 ${time} 次,这是其他参数:${data}`);
        }, 1000 * time);	// 还是每秒执行一次,不是累加的
    })(i, '其他参数')   // 实参,这里把要用的参数传进去
}

作用域问题
在js中,一个{}就是一个代码块,我们在for循环中定义的i变量,全局可以使用,循环中的每一次给i赋值,都是给全局变量i赋值,每次循环都会产生一个代码块,每个代码块中的都是一个新的变量

方案2

将var改为let

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

let的作用域是块作用域,每次js把setTimeout放到队列的同时,let定义的 i 的值也会跟随setTimeout进去队列。所以每次循环后队列里的setTimeout里的let i 的值是不一样的。而var定义的 i 作用域是全局的,当程序运行到setTimeout时获取的全局var i 的值已经变成3了。

方案3

不使用setTimeout,自己写一个“休眠”函数。

function sleep(delay) {
    var start = (new Date()).getTime();
    while((new Date()).getTime() - start < delay) {
        continue;
    }
}

for(var i = 0; i < 3; i++) {
	//调用自己写的sleep函数
	sleep(1000);
	console.log(i);
};