文章首发于 szhshp的第三边境研究所 ,转载请注明
一道Javascript面试题引发的血案
先来看几道面试题,公司的开发们都尝试做了一下,然而基本没有人能够全部答对。
覆盖的考点很多,也有一些难度,题目挺有意思建议手动执行一边玩玩。
Question 1
    for (var i = 0; i <5 ; i++) {
        setTimeout(function(){
            console.log(i)
        ),1000}
    }
    console.log(i)
- Q:这道题目会输出什么?
 - A:这道题目还比较简单,如果对Javascript稍微有一点深入的同学都会发现这道题目循环里面出现了闭包,因此输出的数字是完全相同的,最后的输出也是完全相同的。
 - 考点:闭包,(伪)异步
 
Question 2
    for (let i = 0; i <5 ; i++) { //注意var变成了let
        setTimeout(function(){
            console.log(i)
        },1000)
    }
    console.log(i)
- 
Q:这道题目会输出什么?
 - 
A:这道题目其实是个坑。首先题目与Q1的区别就是变量i的定义改为了关键字let,使用let的时候会将变量限制在循环之中,因此第二个输出其实会报错。另外
setTimeout实现了(伪)异步,同时因为let将变量作用域进行了控制,破坏了闭包结构,因此会按照正常顺序输出。关于let关键字[1]
Use the let statement to declare a variable, the scope of which is restricted to the block in which it is declared. You can assign values to the variables when you declare them or later in your script.
A variable declared usingletcannot be used before its declaration or an error will result.. - 
考点:闭包,(伪)异步,作用域
 
Question 3
同样是Q1的代码
    for (var i = 0; i <5 ; i++) {  //DO NOT MODIFY
        setTimeout(function(){ //DO NOT MODIFY
            console.log(i) 
        },1000)
    }
    console.log(i)  //DO NOT MODIFY
- 
Q:修改上述代码(部分行不允许修改,可以在代码间插入),以实现“每隔一秒输出一个数字并且顺序为0-5”
 - 
A
- 首先考到了破坏闭包结构,破坏闭包的方法很多,最简单的是将跨域变量转换成范围内的变量
 - 其次考到了
setTimeout事件队列的处理 
for (var i = 0; i <5 ; i++) { (function(i){ setTimeout(function(){ console.log(i) },1000*i) })(i) //将i作为参数传入匿名函数,如此破坏了闭包内跨域访问 } setTimeout(function (){ console.log(i); }, 5000); //强行将5放到5sec后输出 - 
考点:闭包,(伪)异步,作用域,事件队列
 
Question 4
window.setTimeout(function (){
    console.log(2)
},1);
//Ouput for a long time
for (var i = 0; i < 1000; i++) {
    console.log('');
};
console.log(1)
window.setTimeout(function (){
    console.log(3)
},0);
- Q:这道题目会输出什么?
 - A:可能有些同学会记得,setTimeout是一个回调函数,因此无论延时多少结果都是最后输出。
 - 考点:(伪)异步,事件队列
 
Question 5
这道题目其实是其他地方抄袭来的[2],正好和之前考点有一定重叠因此一起放了过来:
    setTimeout(function(){console.log(4)},0);
    new Promise(function(resolve){
        console.log(1)
        //time consuming ops
        for( var i=0 ; i<10000 ; i++ ){
            i==9999 && resolve();
        }
        console.log(2)
    }).then(function(){
        console.log(5)
    });
    console.log(3);
- 
Q:这道题目会输出什么?
 - 
A:输出是12354
关于这个输出,有如下几个逻辑:
- 4是
setTimeOut.callback的输出,加入MacroTask末端, - 输出1
 - 执行
Promise.resolve()将输出5的callback放到MicroTask中(注意这里不是MacroTask) - 输出2
 - 输出3
 - 
MacroTask首个任务执行完毕 - 查找
MicroTask里面有没有任务,发现有,执行,输出5 - 查找
MacroTask里面有没有任务,发现有,执行,输出4 - 查找
MicroTask里面有没有任务,发现没有,可以休息了 - 查找
MacroTask里面有没有任务,发现没有,可以睡觉了 - 执行完毕
 
 - 4是
 
关于事件循环/关于macrotask和microtask[3]
简介
一个事件循环(EventLoop)中会有一个正在执行的任务(Task),而这个任务就是从 macrotask 队列中来的。在whatwg规范中有 queue 就是任务队列。当这个 macrotask 执行结束后所有可用的 microtask 将会在同一个事件循环中执行,当这些 microtask 执行结束后还能继续添加 microtask 一直到真个 microtask 队列执行结束。
怎么用
基本来说,当我们想以同步的方式来处理异步任务时候就用 microtask(比如我们需要直接在某段代码后就去执行某个任务,就像Promise一样)。
其他情况就直接用 macrotask。
两者的具体实现
- macrotasks: 
setTimeoutsetIntervalsetImmediateI/OUI渲染 - microtasks: 
Promiseprocess.nextTickObject.observeMutationObserver 
从规范中理解
规范:https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
- 一个事件循环(event loop)会有一个或多个任务队列(task queue) task queue 就是 macrotask queue
 - 每一个 event loop 都有一个 microtask queue
 - task queue == macrotask queue != microtask queue
 - 一个任务 task 可以放入 macrotask queue 也可以放入 microtask queue 中
 - 当一个 task 被放入队列 queue(macro或micro) 那这个 task 就可以被立即执行了
 
再来回顾下事件循环如何执行一个任务的流程
当执行栈(call stack)为空的时候,开始依次执行:
- 把最早的任务(task A)放入任务队列
 - 如果 task A 为null (那任务队列就是空),直接跳到第6步
 - 将 currently running task 设置为 task A
 - 执行 task A (也就是执行回调函数)
 - 将 currently running task 设置为 null 并移出 task A
 - 执行 microtask 队列
- 在 microtask 中选出最早的任务 task X
 - 如果 task X 为null (那 microtask 队列就是空),直接跳到 g
 - 将 currently running task 设置为 task X
 - 执行 task X
 - 将 currently running task 设置为 null 并移出 task X
 - 在 microtask 中选出最早的任务 , 跳到 b
 - 结束 microtask 队列
 
 - 跳到第一步
 
上面就算是一个简单的 event-loop 执行模型
再简单点可以总结为:
- 在 macrotask 队列中执行最早的那个 task ,然后移出
 - 执行 microtask 队列中所有可用的任务,然后移出
 - 下一个循环,执行下一个 macrotask 中的任务 (再跳到第2步)
 
其他
- 当一个task(在 macrotask 队列中)正处于执行状态,也可能会有新的事件被注册,那就会有新的 task 被创建。比如下面两个
1. promiseA.then() 的回调就是一个 task
1. promiseA 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
1. promiseA 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
1. setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况 - microtask queue 中的 task 会在事件循环的当前回合中执行,因此 macrotask queue 中的 task 就只能等到事件循环的下一个回合中执行了
 - click ajax setTimeout 的回调是都是 task, 同时,包裹在一个 script 标签中的js代码也是一个 task 确切说是 macrotask。
 








网友评论