close

JS是個單執行緒的語言,也就是程式在執行時同一時間只能由一個主線程處理任務,主因是JS在瀏覽器執行,如果是多線程處理,很難保證所有操作的一致性,因此後來Web worker的出現允許我們將耗時計算且不含DOM 元件的代碼放入背景執行以達到不阻塞主線程的功用並提高執行效率。

那麼JS在執行時如何在單線程的情況下處理I/O, api請求等較耗時的任務又能達到非阻塞的效果?這裡就要提到Event Loop(事件循環)的概念:JS在執行腳本時,會將同步執行的程式碼依序加入Call Stack(呼叫堆疊)中,為後進先出,如果當前處理的是一個方法,JS會在呼叫堆疊中建立一個方法的執行環境,進入環境中執行,執行完後銷毀並回到上一個方法的環境執行直到堆疊中無任務為止。如果遇到異步任務返回結果,JS會將此請求放入Task Queue(事件隊列),等到堆疊中沒有方法要執行,才會檢查事件隊列是否有任務要執行,再將回調函數放入堆疊。

而JS會根據異步事件的類型將事件放到宏任務(MacroTask)或微任務(MicroTask)的事件隊列中,宏任務有:I/O, script, setTimeout, setInterval, setImmediate, requestAnimationFrame。微任務有:Promise.then/.catch/.finally, await後。微任務是宏任務的一個步驟,也就是說進入腳本執行就是第一個宏任務,接著執行宏任務底下的微任務直到微任務隊列為空,接著開始新一輪循環,如果遇到一個新的宏任務,此任務會進入宏任務的事件隊列等待下一輪執行。因此順序是:宏任務 -> 微任務 -> 渲染。註:同一次的事件循環微任務優先級會高於宏任務,當所有同步代碼執行完,事件循環會優先處理微任務,那為何前面提到順序是宏任務先呢?因為執行腳本就是第一個宏任務,這部分執行完,事件循環就會處理微任務(promise callback),再來才是宏任務(timeout callback)。

【實作】 JS 事件循環

接著我們來看個例子:

執行順序如下:
①此代碼為做為一個script,執行第一個宏任務
②遇到setTimeout,將回調放入宏任務隊列:console.log(3)
③執行全局代碼:印出console.log(7)
④執行foo(),這裡async為es7引入的,本質為Promise的另一種處理形式,在async函數裡,await之前的代碼是同步執行的,印出console.log(1)。await後的代碼都是Promise.then()的回調,回調放入微任務隊列:console.log(2)。接著then()中的回調也是放入微任務隊列中:console.log(8)
⑤執行bar(),立即執行console.log(4),遇到Promise構造函數,為同步執行,遇到setTimeout,將回調放入宏任務隊列:console.log(5),接著立即執行console.log(6)
⑥執行全局代碼:印出console.log(9),到這邊script作為宏任務執行結束。
⑦接著檢查微任務隊列是否有可執行的任務,依序印出:console.log(2),console.log(8)
⑧開始新一輪循環,檢查宏任務隊列是否有可執行的任務,依序印出:console.log(3),console.log(5)

結果為:7 1 4 6 9 2 8 3 5

參考資料:
1. Difference between microtask and macrotask within an event loop context
2. 事件循环:微任务和宏任务
3. 前端开发必知必会:从工程核心到前沿实战

arrow
arrow
    文章標籤
    javascript
    全站熱搜

    龍眼的歡樂人生 發表在 痞客邦 留言(0) 人氣()