一、JS單線程、異步、同步概念
從上一篇說明vue nextTick的文章中,多次出現“事件循環”這個名詞,簡單說明了事件循環的步驟,以便理解nextTick的運行時機,這篇文章將更為詳細的分析下事件循環。在此之前需要了解JS單線程,及由此產生的同步執行環境和異步執行環境。
眾所周知,JS是單線程(如果一個線程刪DOM,一個線程增DOM,瀏覽器傻逼了~所以只能單着了),雖然有webworker醬紫的多線程出現,但也是在主線程的控制下。webworker僅僅能進行計算任務,不能操作DOM,所以本質上還是單線程。
單線程即任務是串行的,后一個任務需要等待前一個任務的執行,這就可能出現長時間的等待。但由於類似ajax網絡請求、setTimeout時間延遲、DOM事件的用戶交互等,這些任務並不消耗 CPU,是一種空等,資源浪費,因此出現了異步。通過將任務交給相應的異步模塊去處理,主線程的效率大大提升,可以並行的去處理其他的操作。當異步處理完成,主線程空閑時,主線程讀取相應的callback,進行后續的操作,最大程度的利用CPU。此時出現了同步執行和異步執行的概念,同步執行是主線程按照順序,串行執行任務;異步執行就是cpu跳過等待,先處理后續的任務(CPU與網絡模塊、timer等並行進行任務)。由此產生了任務隊列與事件循環,來協調主線程與異步模塊之間的工作。
二、事件循環機制

事件循環示例圖
如上圖為事件循環示例圖(或JS運行機制圖),流程如下:
step1:主線程讀取JS代碼,此時為同步環境,形成相應的堆和執行棧;
step2: 主線程遇到異步任務,指給對應的異步進程進行處理(WEB API);
step3: 異步進程處理完畢(Ajax返回、DOM事件處罰、Timer到等),將相應的異步任務推入任務隊列;
step4: 主線程執行完畢,查詢任務隊列,如果存在任務,則取出一個任務推入主線程處理(先進先出);
step5: 重復執行step2、3、4;稱為事件循環。
執行的大意:
同步環境執行(step1) -> 事件循環1(step4) -> 事件循環2(step4的重復)…
其中的異步進程有:
a、類似onclick等,由瀏覽器內核的DOM binding模塊處理,事件觸發時,回調函數添加到任務隊列中;
b、setTimeout等,由瀏覽器內核的Timer模塊處理,時間到達時,回調函數添加到任務隊列中;
c、Ajax,由瀏覽器內核的Network模塊處理,網絡請求返回后,添加到任務隊列中。
三、任務隊列
如上示意圖,任務隊列存在多個,同一任務隊列內,按隊列順序被主線程取走;不同任務隊列之間,存在着優先級,優先級高的優先獲取(如用戶I/O);
3.1、任務隊列的類型
任務隊列存在兩種類型,一種為microtask queue,另一種為macrotask queue。
圖中所列出的任務隊列均為macrotask queue,而ES6 的 promise[ECMAScript標准]產生的任務隊列為microtask queue。
3.2、兩者的區別
microtask queue:唯一,整個事件循環當中,僅存在一個;執行為同步,同一個事件循環中的microtask會按隊列順序,串行執行完畢;
macrotask queue:不唯一,存在一定的優先級(用戶I/O部分優先級更高);異步執行,同一事件循環中,只執行一個。
3.3、更完整的事件循環流程
將microtask加入到JS運行機制流程中,則:
step1、2、3同上,
step4:主線程查詢任務隊列,執行microtask queue,將其按序執行,全部執行完畢;
step5:主線程查詢任務隊列,執行macrotask queue,取隊首任務執行,執行完畢;
step6:重復step4、step5。
microtask queue中的所有callback處在同一個事件循環中,而macrotask queue中的callback有自己的事件循環。
簡而言之:同步環境執行 -> 事件循環1(microtask queue的All)-> 事件循環2(macrotask queue中的一個) -> 事件循環1(microtask queue的All)-> 事件循環2(macrotask queue中的一個)...
利用microtask queue可以形成一個同步執行的環境,但如果Microtask queue太長,將導致Macrotask任務長時間執行不了,最終導致用戶I/O無響應等,所以使用需慎重。
四、示例、驗證
console.log('1, time = ' + new Date().toString()) setTimeout(macroCallback, 0); new Promise(function(resolve, reject) { console.log('2, time = ' + new Date().toString()) resolve(); console.log('3, time = ' + new Date().toString()) }).then(microCallback); function macroCallback() { console.log('4, time = ' + new Date().toString()) } function microCallback() { console.log('5, time = ' + new Date().toString()) }
結合第二節與第三節的分析,此處的執行流程應為:
同步環境:1 -> 2 -> 3
事件循環1(microCallback):5
事件循環2(macroCallback):4
運行結果如下:
運行結果與預期一致,驗證了在不同類型的任務隊列中,microtask queue中的callball將優先執行。
總結:由此我們了解事件循環的機制,同時了解了任務隊列、JS主線程、異步操作之間的相互協作;同時認識了兩種任務隊列:macrotask queue、microtask queue,它們由不同的標准制定,microtask queue對應ECMAScript的promise屬性(ES6)和 DOM3的MutationObserver,文中說明了兩者在事件循環中的運行情況及區別;在今后的異步操作中,通過靈活運用不同的任務隊列,提升用戶交互性能,給出更加的響應和視覺體驗;同時,通過JS的事件循環機制,可以更清楚JS代碼的執行流,從而更好的控制代碼,更有效、更好的為業務服務。