最近羅列了一些軟件開發基礎知識點,計劃逐一的、完全的理解每個知識點,併爲每一個知識點寫一篇詳細的,圖文並茂的文章。這篇是關於瀏覽器環境下 JS 的 Event Loop 機制(若有錯誤,歡迎指出)。html
咱們常說 JS 是單線程語言,可是別忘了常見的瀏覽器內核可都是多線程的,多個線程間會進行不斷通信,一般會有以下幾個線程:html5
在大多數解釋 JS Event Loop 的文章中,鮮有談及 Miscrotask 和 Macrotask 這兩個概念,但這兩個概念倒是很是的重要,我在翻閱 Zone.js Primer 時,裏面就常常會說起這兩個概念,當時也是看的雲裏霧裏的,這也是我寫這篇文章的緣由之一。web
setTimeout(function () {
console.log('timeout1');
}, 0);
console.log('start');
Promise.resolve().then(function () {
console.log('promise1');
Promise.resolve().then(function () {
console.log('promise2');
});
setTimeout(function () {
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('timeout2')
}, 0);
});
console.log('done');
複製代碼
以上代碼最後會輸出什麼呢?若是你能很快的回答出來,你大概就已經掌握了 Event Loop 的實際運用了,若是回答不出,那可能還得接着往下看。api
問題:是先執行 then( ) 中的回調函數呢,仍是 setTimeout( ) 中的回調函數呢?promise
答案:先執行前者。由於 Promise.prototype.then( ) 是 Microtask ,而 setTimeout( ) 是 Macrotask 。至於爲何先執行 Miscrotask ?繼續日後看~瀏覽器
在 JS 線程中程序的每個調用都被當作是一個任務(task) ,全部的任務被分紅許多類型且存放在對應類型的隊列中,爲了方便理解,我把這些任務隊列分紅三類:服務器
Micro-task queue: 存放 microtask 的回調函數。多線程
Macro-task queue: 存放 macrotask 的回調函數 。app
Other-task queue: 這是一個我我的抽象出來隊列,實際並不存在,假設該隊列用來存放除了 microtask 和 macrotask 外的全部任務。webapp
Microtask 和 Macrotask 的區別就是執行順序上的區別。簡單的說,JS 線程會先處理 other-task queue 上的任務,處理完了以後,再去處理 micro-task queue 上的任務,最後才處理 macro-task queue 上的任務。至於 JS 線程具體的執行細節,後面會詳細的進行描述。
如下是常見的 Microtask 和 Macrotask:
Microtask :Promise.prototype.then( )、MutationObserver.prototype.observe( ) 等 。
Macrotask :setTimeout( )、setImmediate( )、XMLHttpRequest.prototype.onload( ) 等。
如上,根據我的的理解,我畫了一個瀏覽器環境下 JS 實現 Event Loop 大體模型圖,具體含義以下:
1 獲取執行的任務,執行步驟 1.1
1.1 判斷 other-task queue 中是否有任務,若是有,獲取最先的任務而後執行步驟 2 ,不然執行步驟 1.2 。
1.2 判斷 micro-task queue 中是否有任務,若是有,獲取最先的任務而後執行步驟 2 ,不然執行步驟 1.3 。
1.3 判斷 macro-task queue 中是否有任務,若是有,獲取最先的任務而後任何執行步驟 2 ,不然執行步驟 3 。
2 將取到的任務放到 call stack 並執行,執行完以後再執行步驟 1 (值得注意的是,在執行的過程當中,是會不斷的更新全部的 task queue ,由於 call stack 中正在執行的任務內部也可能存在普通任務、microtask 和 macrotask ,執行任務的過程能夠理解爲一個遞歸過程,若是無限遞歸,call stack 上待執行的任務就會不斷累積而溢出,這也就是常見的 Maximum call stack size exceeded 錯誤)。
3 線程會處理其餘工做,例如:不斷同步「事件觸發線程」的狀態,一旦有事件觸發,即查看觸發事件「target」有沒有對應事件的監聽器任務,若是有,則選中該任務並執行步驟 2 。須要注意的是,並非只有執行了步驟 1.3 後纔會執行當前步驟,JS 線程確定還會在的某個時候去同步其餘線程的狀態的。
接下來,若是仔細想,可能會產生一個疑問:JS 進程是如何更新 micro-task queue 和 macro-task queue 這兩個隊列的呢 ?
根據個人理解,micro-task queue 和 other-task queue 都是「同步」更新的,而 macro-task queue 是「異步」更新。如下是 macro-task queue 更新的具體流程(以 setTimeout 爲例):
目前廣泛對異步的解釋多是:執行調用,若是當即獲得結果就是同步調用,不然爲異步調用。
在 JS 環境中,我我的實際上是不一樣意這個解釋的。
首先,根據以上的解釋,setTimeout( )、Promise.prototype.then( ) 、http 請求和各種瀏覽器事件,這些都被認爲是異步的。但我卻不這麼認爲,我認爲瀏覽器事件不是異步的。如下代碼即是理由:
// html: <button id="btn">click</button>
// js
var btn = document.getElementById('btn');
setTimeout(function () {
console.log('timeout')
}, 0);
Promise.resolve().then(function () {
console.log('promise');
});
btn.addEventListener('click', function () {
console.log('click');
});
btn.click();
console.log('done');
複製代碼
若是瀏覽器事件是異步的,無論後續會打印出什麼,第一個打印的必然是 done ,而實際的打印結果爲:click done promise timeout
。
也就是說,JS 認爲瀏覽器事件並不是異步。
由此,我我的對異步的解釋是:在知足調用所需的外在條件的狀況下,執行調用,當即得到結果的就爲同步調用,不然爲異步調用。
根據這個理解,當咱們發起的一個 http 請求時,假設服務器以光速返回請求結果,XMLHttpRequest 對象的 onload 方法會當即執行嗎?,顯然不會,因此 http 請求爲異步調用。這也是爲何我在以上分析 Event Loop 中的任務隊列時並無將 event-task queue 拎出來的緣由。所以,對於異步調用的判斷能夠是這樣:若是某個調用屬於 microtask 或是 macrotask 中的其中一個,那麼這個調用就是異步調用。
有人可能會注意到,這篇文章常常出現「我認爲」和「我理解」,這並不是是我對本身不自信,而是我想表達一個見解:在翻閱別人的技術文章的時候,務必保持獨立思考的能力,就算文章的做者是業界有名的大牛,也不能沒原因的「深信不疑」,對對應的技術點務必在自個腦中裏創建一個能夠自圓其說的模型。至於我爲何會表達這個見解,是由於我找翻閱大量的過程當中,發現大多數關於 JS Event Loop 的文章或多或少都有一些粗糙或是錯誤,若是我只看其中的某一篇,我很大的機率會有創建一個錯誤的 Event Loop 模型。固然,就我當前的理解,仍是可能會有些許錯誤。Anyway ,仍是那句話:保持獨立思考,與各位共勉。
Done.👊