文章首發于 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
- 首先考到了破壞閉包結構,破壞閉包的方法很多,最簡單的是將跨域變量轉換成范圍內的變量
-
其次考到了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里面有沒有任務,發現沒有,可以睡覺了
- 執行完畢
關于事件循環/關于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)為空的時候,開始依次執行:
- 把最早的任務(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。
參考文獻
[^3]: let 語句 (JavaScript) .aspx)
[^2]: https://www.zhihu.com/question/36972010
[^1]: https://github.com/ccforward/cc/issues/48
文章列表