倒底如何解釋Event Loop
開頭
首先我們從它的運行機制Event Loop來說起。
首先科普一些基礎知識。
程序(進程)、執行緒(線程)
進程 Process
Process 進程則是指被執行且載入記憶體的 program。Process 也是 OS 分配資源的最小單位,可以從 OS 得到如 CPU Time、Memory…等資源,意思是這個 process 在運行時會消耗多少 CPU 與記憶體。文章一開始放了一張 MacOS 活動監視器的截圖,相信不管是使用哪種作業系統的讀者都有看過類似的介面,而監視器中列出的是你的電腦正在執行的應用程式,而它們其實就是一個個 process。
線程 Thread
線程可以想像成存在於 process 裡面,而一個進程裡至少會有一個線程,前面有說 process 是 OS 分配資源的最小單位,而 thread 則是作業系統能夠進行運算排程的最小單位,也就是說實際執行任務的並不是進程,而是進程中的線程,一個進程有可能有多個線程,其中多個線程可以共用進程的系統資源。
一個問題
- 在多線程操作下實現應用的並行處理,能夠以更高的CPU擴展提高整個程序的性能和語言處理能力都特別是現在,但JavaScript卻以單線程執行,為什麼呢?
- 答:JavaScript作為腳本語言,最初是為了避免複雜的同步問題(做人嘛,還是簡單點好,也一樣),如果JavaScript同時有兩個線程,一個線程中執行在某個DOM節點上添加,另一個線程執行刪除這個節點,瀏覽器會……
所以 JavaScript 的單線程是這門語言的核心,未來也不會改變。
事情說,那HTML5的新特性Web Worker,可以創建多線程呀~
是的,為了解決鸚鵡的這個操作(多重循環、複雜的這個操作操作),HTML5提出Web Worker,它會在當前的js執行主線程中開闢出一個額外的線程來運行js文件新的線程和js主線程之間不會互相影響,同時提供了數據交換的接口:postMessage和onMessage。
語言的設計和生活中的現實情況很像,IO設備(輸入輸出)很慢(比如Ajax),那麼語言的設計者鬧這一點,就在主線程中掛起等待中的任務,先運行沒有的任務,等IO設備有了結果,再把掛起的任務執行下去。
從上到下可以看到,在主線程運行時,會產生堆(堆)和棧(棧)。
堆中存的是我們聲明的對像類型的數據,棧中存的是基本數據類型以及函數執行時的運行空間。
棧中的代碼會調用各種外部API,它們在任務中加入各種事件(onClick,onLoad,onDone),只要棧中的代碼執行完畢(js引擎存在監控流程進程,會持續不斷的檢查主線程)執行棧是否為空),主線程就返回讀取任務,在按順序執行這些響應的響應函數。
到主線程從任務這個任務中讀取事件,所以這個過程是循環不斷的,所以這種運行機制又成為事件循環(事件循環)。
步任務和異步任務
我們將任務分為同步任務和異步任務。
同步任務就是在主線程上執行的任務,可以執行一個再執行下一個。
異步任務則不進入主線程,可能先在事件表中註冊函數,當滿足觸發條件後,可以進入任務召喚來執行。此任務將進入主線程執行。
- 舉例
1 | console.log(a); |
1.console.log(a)是同步任務,進入主線程執行,印出a。
2.setTimeout是異步任務,先被動態事件表中註冊,1000ms後進入任務探測。
3.console.log(c)是同步任務,進入主線程執行,印出c。
當a,c被印出後,主線程去事件中找到setTimeout裡的函數,並執行,印出b。
宏任務和微任務
同步任務和異步任務的劃分細節宏觀,具體的分類方式是任務(Macrotask)和微任務(Microtask)。
宏任務包括:script(整體代碼),I/O,setTimeout,setInterval,requestAnimationFrame,setImmediate。
設置立即只存在於節點中,requestAnimationFrame 只存在於瀏覽器中。
微任務包括: Promise,Object.observe(已廢棄),MutationObserver(html5新特性),process.nextTick。
還有process.nextTick只存在於Node中,MutationObserver只存在於瀏覽器中。
UI Rendering不屬於宏任務,也不屬於微任務,它是一個與微任務類似的一個操作步驟。
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
這些分類的執行,執行一個宏任務,過程中遇到的微任務時,將其現在微當前的事件捕捉,執行完後任務中的任務,具體可以查看詳細任務的細節,具體執行內容的微任務。如果還有宏任務的話,再重新開啟宏任務……
1 | setTimeout(function() { |
- 再舉例
1.首先執行腳本下的宏任務,遇到setTimeout,將其原生宏任務的召喚裡。
- 遇到Promise,new Promise直接執行,印出b。
3.遇到然後方法,是微任務將其可以微任務的里。
4.遇到console.log(‘d’),直接印出。
5.本輪宏任務發現執行完畢,查看微任務,然後方法裡的函數,印出c。
6.本輪事件循環全部完成。
7.下引發循環,先執行宏任務,宏任務產生一個,印出一個setTimeout。
瘋狂的
1 | console.log('a'); |
好,我們來逐步分析。
第一輪事件循環:
第一個宏任務(整體腳本)進入主線程,console.log(‘a’),印出a。
遇到setTimeout,其觸發功能進入宏任務,暫定義為setTimeout1。
遇到process.nextTick(),其原因函數被傳到微任務請求,暫定義為process1。
遇到Promise,new Promise直接執行,印出g。 then進入微任務,暫定義為then1。
遇到setTimeout,其觸發功能進入宏任務,暫定義為setTimeout2。
這時候我們看一下兩個任務中的情況
宏任務請求:setTimeout1、Timeout2
微任務請求:process1、then1
第一輪宏任務執行完畢,印出出a和g。
全部執行,印出f和h。
第一輪事件循環完畢,印出出a、g、f和h。
第二輪事件循環:
從setTimeout1宏任務開始,首先是console.lob(‘b’),印出b。
遇到process.nextTick(),進入微任務,暫定義為process2。
Promise直接執行,然後進入微任務輸出,暫定義為then2。
這兩個任務中
宏任務請求:setTimeout2
微任務請求:process2、 then2
第二輪宏任務執行完畢,印出出b和d。
全部執行,印出和e。
第二輪事件循環完畢,印出出b、d、c和e。
第三輪事件循環
執行setTimeout2,遇到console.log(‘i’),印出i。
遇到process.nextTick(),進入微任務,暫定義為process3。
new Promise直接執行,印出k。
then進入微任務,暫定義為then3。
這兩個任務中
宏任務請求:空
微任務請求:process3、then3
第三輪宏任務執行完畢,印出出i和k。
全部執行,印出j和l。
第三輪事件循環完畢,印出出i、k、j和l。
到此為止,三輪事件循環結束,最終輸出結果為:
a、g、f、h、b、d、c、e、i、k、j、l