for循环中setTimeout的执行问题
有几种情况,看到可能就第一反应就知道是什么,但是还不一定对,首先我们先回顾一下js单线程和异步调用的知识
JS是单线程环境,也就是说代码的执行是从上到下,依次执行。也就是同一个时间只能做一件事。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。JavaScript将所有任务分成两种,一种是同步任务,另一种是异步任务。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
在所有同步任务执行完之前,任何的异步任务是不会执行的。 而setTimeout就是一个异步任务,所以会先执行for循环这个同步任务,把setTimeout()放进任务队列中等待主线程的for循环执行完毕,一旦"执行栈"中的所有同步任务执行完毕(循环结束后此时i=10)就会从队列中取出setTimeout()
for循环一次碰到一个 setTimeout(),并不是马上把setTimeout()拿到异步队列中,而要等到一秒后,才将其放到任务队列里面。
异步执行的运行机制如下:
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会循环反复。
以下四种是会放入到异步队列中
- setTimeout和setlnterval
- DOM事件
- ES6中的Promise
- Ajax异步请求
题目一
for(var i=0;i<10;i++){
setTimeout(console.log(i),0);
}
冷不丁一想,看着就是十个10,那就错了,答案是0、1、2、3、4、5、6、7、8、9;
我们分析一下这个问题,看setTimeout里面的第一个参数,并不是一个正常的函数,这个函数你可以理解为下面的这个函数,是一个匿名自调用函数,也就是一个匿名立即执行函数(IIFE),console.log()和console.log是不一样的,前者是立即执行,后者不是,后者只是个函数名,该题目是for循环执行的时候console.log(i)是一起同步执行的,setTimeout是后面异步队列中执行的,所以会直接打印出0、1、2、3、4、5、6、7、8、9。
(function() {
console.log(1)
})()
题目二
这个题目,会直接打印出十个10,原因在于加了双引号的console.log()不再是立即执行函数。setTimeout() 会判断第一个参数是否是[function],如果不是,则会尝试将它当做字符串处理。也就是说,console.log(i)执行后的返回值转为字符串。
for(var i=0;i<10;i++){
setTimeout("console.log(i)",1000);//连续的10个10
}
题目三
这个问题答案和题目二是一样的,但是机制是不一样的,这个实际上是for循环先执行完,然后将setTimeout放入任务队列中,然后执行setTimeout中的代码,此时for中的i已经变成了10,所以会打印十次10.
for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i); //连续的10个10
},0);
}
题目四
我们看一下这两段代码,实际上结果是相同的
- 第一段代码是因为let关键字,let会创建一个块级作用域,也就是说在for循环中,let的i,会生成十个不同的i,每一个不受另外的i的影响,所以setTimeout异步队列后执行的时候,打印的每个i都是不同的,所以就会打印出0到9
- 第二段我们可以看出实际上是es5的实现块级作用域的方法,也就是创建一个和题目一一样的立即执行函数,立即执行函数中就会马上打印出for循环每一次循环的i的值
for(let i=0;i<10;i++){
setTimeout(function(){
console.log(i); //0到9
},0);
}
var func = [];
for(var i = 0; i < 10; i++) {
func.push((function(value){
return function() {
console.log(value)// 0-9
}
})(i));
}
func.forEach((func) => {
func();
})
总结
这几个题目基本上把所有的for循环中嵌套setTimeout,和let关键字所能考察的知识都概括进去了,可以多总结,多看几遍,就能深入理解其中的含义了