JavaScript setTimeout要理解( 二 )


第一次循环,隔一秒输出1;
第二次循环,隔两秒输出2;
第三次循环,隔三秒输出3;
第四次循环,隔四秒输出4;
第五次循环,隔五秒输出5;
或者还有同学预期的结果是分别输出数字1~5,每秒一次,每次一个 。
但实际结果大家去控制台打印了都知道:以一秒的频率连续输出五个6!
相信对于很多童鞋第一次看到这个结果是懵的,包括我第一次看到结果是懵逼的!
然而还没等你反应过来,面试官又要求你改动一下代码,要它以一秒的频率分别输出1,2,3,4,5 。如果你不了解或者没有深入理解JS中的作用域、闭包以及事件循环,那么就可以和面试官说拜拜了 。
这道题涉及到的知识点我上面已经提到过两次,这里我们还是先简单地过一下这些知识点:
1、作用域:这里我引用《你不知道的javascript》中的一个比喻,可以把作用域链想象成一座高楼,第一层代表当前执行作用域,楼的顶层代表全局作用域 。我们在查找变量时会先在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,如果还是没有找到就继续向上找,以此类推 。到达顶层后(全局作用域),可能找到了你所需的变量,也可能没找到,但无论如何查找过程都将停止 。
2、闭包:我的理解是在传递函数类型的变量时,该函数会保留定义它的所在函数的作用域 。读起来可能比较绕,或者可以简单的这么理解,A函数中定义了B函数并且它返回了B函数,那么不管B函数在哪里被调用或如何被调用,它都会保留A函数的作用域 。
3、事件循环:这个概念深入起来很复杂,下面新开一个段落只说一些跟本文相关的内容 。
说起事件循环,不得不提起任务队列 。事件循环只有一个,但任务队列可能有多个,任务队列可分为宏任务(macro-task)和微任务(micro-task) 。XHR回调、事件回调(鼠标键盘事件)、setImmediate、setTimeout、setInterval、indexedDB数据库操作等I/O以及UI rendering都属于宏任务(也有文章说UI render不属于宏任务,目前还没有定论),process.nextTick、Promise.then、Object.observer(已经被废弃)、MutationObserver(html5新特性)属于微任务 。注意进入到任务队列的是具体的执行任务的函数 。比如上述例子setTimeout()中的timer函数 。另外不同类型的任务会分别进入到他们所属类型的任务队列,比如所有setTimeout()的回调都会进入到setTimeout任务队列,所有then()回调都会进入到then队列 。当前的整体代码我们可以认为是宏任务 。事件循环从当前整体代码开始第一次事件循环,然后再执行队列中所有的微任务,当微任务执行完毕之后,事件循环再找到其中一个宏任务队列并执行其中的所有任务,然后再找到一个微任务队列并执行里面的所有任务,就这样一直循环下去 。这就是我所理解的事件循环 。来,还是看个栗子:
console.log('global')setTimeout(function () { console.log('timeout1') new Promise(function (resolve) { console.log('timeout1_promise') resolve() }).then(function () { console.log('timeout1_then') })},2000)for (var i = 1;i <= 5;i ++) { setTimeout(function() { console.log(i) },i*1000) console.log(i)}new Promise(function (resolve) { console.log('promise1') resolve() }).then(function () { console.log('then1')})setTimeout(function () { console.log('timeout2') new Promise(function (resolve) { console.log('timeout2_promise') resolve() }).then(function () { console.log('timeout2_then') })}, 1000)new Promise(function (resolve) { console.log('promise2') resolve()}).then(function () { console.log('then2')})我们来一步一步分析以上代码:
1)、首先执行整体代码,“global”会被第一个打印出来 。这是第一个输出.
2)、执行到第一个setTimeout时,发现它是宏任务,此时会新建一个setTimeout类型的宏任务队列并派发当前这个setTimeout的回调函数到刚建好的这个宏任务队列中去,并且轮到它执行时要延迟2秒后再执行
3)、代码继续执行走到for循环,发现是循环5次setTimeout(),那就把这5个setTimeout中的回调函数依次派发到上面新建的setTimeout类型的宏任务队列中去,注意,这5个setTimeout的延迟分别是1到5秒 。此时这个setTimeout类型的宏任务队列中应该有6个任务了 。再执行for循环里的console.log(i),很简单,直接输出1,2,3,4,5,这是第二个输出 。
4)、再执行到new Promise,Promise构造函数中的第一个参数在new的时候会直接执行,因此不会进入任何队列,所以第三个输出是"promise1",上面有说到Promise.then是微任务,那么这里会生成一个Promise.then类型的微任务队列,这里的then回调会被push进这个队列中 。


推荐阅读