標籤:git llb 開發 console 非同步任務 red java strong blank
文章首發於 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)
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 timefor (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() 的回調就是一個 task1. promiseA 是 resolved或rejected: 那這個 task 就會放入當前事件迴圈回合的 microtask queue1. 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
一道Javascript面試題引發的血案