同学们在刚准备面试时肯定见过一道经典面试题:
for(var i = 0; i < 10; i++) {setTimeOut(function(){console.log(i)})
}
// 输出 10 10 10 10 10 10 10 10 10 10for(let i = 0; i < 10; i++) {setTimeOut(function(){console.log(i)})
}
// 输出 0 1 2 3 4 5 6 7 8 9
有的同学就很疑惑,怎么肥事,这不一样嘛!
然后就去百度查查
然后百度就会告诉你:
第一个变量i是用var声明的,在全局范围内有效,所以全局中只有一个变量i,每次循环时,setTimeOut定时器里指的是全局变量i,而循环里的十个setTimeOut是在循环结束后才执行,所以输出十个10。
第二个变量i是用let声明的,当前的i
只在本轮循环中有效,每次循环的i其实都是一个新的变量,所以setTImeOut定时器的里面的i其实不是同一变量,所以输出0123456789
看完以后
似懂非懂
就算使用var定义的i是全局变量,每次循环都改变全局范围里的i的值,但是循环一次,执行一次setTimeOut啊,不也应该输出当前i值吗?
em…
不管,面试官问我我就照着这么回答就行了,面试官肯定明白我说的什么
其实之前我理解的也是很片面的,直到这两天看见一篇Google大佬的文章,全是英文,看了好半天****,下面👇,我就结合这篇文章给大家讲讲我的理解(瞎讲讲)吧
首先,在理解这个问题之前,我们需要理解一下macro-task(宏任务)和micro-task(微任务)
先给大家举一个例子(大佬举的例子):
console.log('script start');setTimeout(function() {console.log('setTimeout');
}, 0);Promise.resolve().then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
})console.log('script end');
大家来仔细猜一猜执行结果是什么呐?
结果:
script start
script end
promise1
promise2
setTimeout
嘿!有点东西
其实js是单线程的,每个线程有它自己的唯一的事件循环,但是事件循环的任务源可以不唯一。类似setTimeout, promise, ajax, DOM操作等都是典型的任务源,任务队列中的任务便是来自这些任务源。而这些任务源产生的任务又可以分为macro-task(宏任务)和micro-task(微任务)两种。
macro-task(宏任务)
macro-task(宏任务)中的任务都是有时间顺序的,因此浏览器能够有序地从中调度任务并执行。在任务与任务之间,浏览器可能会渲染更新。
macro-task(宏任务)中一个典型就是setTimeout,setTimeout函数等待给定的延迟事件然后将其回调函数推入宏任务Event Queue中。这就是为什么先输出’script end’ 后输出’setTimeout’的原因。
macro-task(宏任务)主要有:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task(微任务)
micro-task(微任务)中的任务在当前函数调用栈中的函数执行完成之后即调度,像promise、mutation都会被推入微任务Event Queue队列中。并且微任务Event Queue队列中的一个任务执行完成后,后续的micro-task(微任务)也会继续执行,直到微任务Event Queue队列为空,这就解释了为什么promise2也会在setTimeout之前输出的原因。
微任务Event Queue队列主要有process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
当宏任务Event Queue队列中的一个任务执行结束时,如果函数调用栈为空,便会开始执行微任务Event Queue队列中的任务,直至微任务Event Queue队列中所有任务执行完毕,然后event loop才会继续执行宏任务Event Queue队列中的下一个任务。
上图文:
接下来,上个厉害的(也是大佬举的例子)
<div class="outer"><div class="inner"></div>
</div>
分别给这两个div加上点击事件
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');new MutationObserver(function () {console.log('mutate');
}).observe(outer, {attributes: true,
});function onClick() {console.log('click');setTimeout(function () {console.log('timeout');}, 0);Promise.resolve().then(function () {console.log('promise');});outer.setAttribute('data-random', Math.random());
}inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
大家再来仔细思考一下执行结果是什么呐?
结果:
click
promise
mutate
click
promise
mutate
timeout
timeout
嘿!有点大东西
上述例子中Dispatch click和setTimeout属于宏任务Event Queue,对应的回调函数被推进宏任务Event Queue队列中,Mutation observer和promise属于微任务Event Queue,对应的回调函数则被推入微任务Event Queue队列中。当点击inner元素时,代码执行执行过程如下所示
-
Dispatch click被推入宏任务Event Queue队列中,当点击inner元素时onClick被推入函数调用栈(Js stack)中,执行上下文进入onClick中,将setTimeout的回调函数推入宏任务Event Queue队列中,Mutation observers和Promise then的回调函数推入微任务Event Queue队列中,并执行输出click。
-
当onClick执行结束后, 函数调用栈为空,将微任务Event Queue队列中的 promise then 的回调函数推入函数调用栈。
-
同样地,当promise then的回调函数执行结束后,将mutation observers的回调函数推入函数调用栈
-
由于事件冒泡机制,父元素outer也会响应点击事件,因此重复1-3步骤,执行结束后如下所示。
-
此时函数调用栈和microtasks中均为空,因此event loop将执行tasks中的下一个任务。
-
再执行tasks中的最后一个任务。
(注:以上例子均在谷歌浏览器中的输出结果)
上图文:
把这两个例子理解好,是不是感觉对 这道经典面试题 豁然开朗,甚至有点小菜一碟了呐、
其实最后总结一下就是:
当浏览器在执行这段
for(var i = 0; i < 10; i++) {setTimeOut(function(){console.log(i)})
}
代码时
先在全局定义变量 i, 然后执行 for 循环,执行一次 for 循环,分别将 i++ 放入函数调用栈队列,setTimeout 放入task队列 一次。
因为需要将函数调用栈队列里的任务执行结束后,再往下执行task任务
所以 i++ 一直在执行,10次 i++ 执行结束, i 的值为10(为什么不是9?因为 i++ 在值为9时,还会进行一次i++操作,最后一个循环完 i 的值为10,不满足条件,不再循环)。
至此,函数调用栈队列任务执行结束,再去执行task里的十个setTimeout任务,2而此时 i 的值为10,所以输出10 个 10。
最后 附上 :Google 大佬文章
感谢大佬!虽然我们只是大佬的搬运工,但是希望在搬运之前理解好我们所搬运的内容,这已经很厉害啦。
以上理解若有偏差,望各位同学们批评指正。