文章出處

文章首發于 szhshp的第三邊境研究所 ,轉載請注明

先來看幾道面試題,公司的開發們都嘗試做了一下,然而基本沒有人能夠全部答對。

覆蓋的考點很多,也有一些難度,題目挺有意思建議手動執行一邊玩玩。

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關鍵字[^3]

    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 using let cannot 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

    1. 首先考到了破壞閉包結構,破壞閉包的方法很多,最簡單的是將跨域變量轉換成范圍內的變量
    2. 其次考到了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

    關于這個輸出,有如下幾個邏輯:

    1. 4是setTimeOut.callback的輸出,加入MacroTask末端,
    2. 輸出1
    3. 執行Promise.resolve()將輸出5的callback放到MicroTask中(注意這里不是MacroTask)
    4. 輸出2
    5. 輸出3
    6. MacroTask首個任務執行完畢
    7. 查找MicroTask里面有沒有任務,發現有,執行,輸出5
    8. 查找MacroTask里面有沒有任務,發現有,執行,輸出4
    9. 查找MicroTask里面有沒有任務,發現沒有,可以休息了
    10. 查找MacroTask里面有沒有任務,發現沒有,可以睡覺了
    11. 執行完畢

關于事件循環/關于macrotask和microtask[^1]

簡介

一個事件循環(EventLoop)中會有一個正在執行的任務(Task),而這個任務就是從 macrotask 隊列中來的。在whatwg規范中有 queue 就是任務隊列。當這個 macrotask 執行結束后所有可用的 microtask 將會在同一個事件循環中執行,當這些 microtask 執行結束后還能繼續添加 microtask 一直到真個 microtask 隊列執行結束。

怎么用

基本來說,當我們想以同步的方式來處理異步任務時候就用 microtask(比如我們需要直接在某段代碼后就去執行某個任務,就像Promise一樣)。

其他情況就直接用 macrotask。

兩者的具體實現

  • macrotasks: setTimeout setInterval setImmediate I/O UI渲染
  • microtasks: Promise process.nextTick Object.observe MutationObserver

從規范中理解

規范: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)為空的時候,開始依次執行:

  1. 把最早的任務(task A)放入任務隊列
  2. 如果 task A 為null (那任務隊列就是空),直接跳到第6步
  3. 將 currently running task 設置為 task A
  4. 執行 task A (也就是執行回調函數)
  5. 將 currently running task 設置為 null 并移出 task A
  6. 執行 microtask 隊列
    1. 在 microtask 中選出最早的任務 task X
    2. 如果 task X 為null (那 microtask 隊列就是空),直接跳到 g
    3. 將 currently running task 設置為 task X
    4. 執行 task X
    5. 將 currently running task 設置為 null 并移出 task X
    6. 在 microtask 中選出最早的任務 , 跳到 b
    7. 結束 microtask 隊列
  7. 跳到第一步

上面就算是一個簡單的 event-loop 執行模型

再簡單點可以總結為:

  1. 在 macrotask 隊列中執行最早的那個 task ,然后移出
  2. 執行 microtask 隊列中所有可用的任務,然后移出
  3. 下一個循環,執行下一個 macrotask 中的任務 (再跳到第2步)

其他

  1. 當一個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 的情況
    
  2. microtask queue 中的 task 會在事件循環的當前回合中執行,因此 macrotask queue 中的 task 就只能等到事件循環的下一個回合中執行了
  3. click ajax setTimeout 的回調是都是 task, 同時,包裹在一個 script 標簽中的js代碼也是一個 task 確切說是 macrotask。

參考文獻

[^3]: let 語句 (JavaScript) .aspx)
[^2]: https://www.zhihu.com/question/36972010 
[^1]: https://github.com/ccforward/cc/issues/48


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()