摘要:本文經過結合官方文檔MDN和其餘博客深刻解析瀏覽器的事件循環機制,而NodeJS有另外一套事件循環機制,不在本文討論範圍中。process.nextTick和setImmediate是NodeJS的API,因此本文也不予討論。
首先,先了解幾個概念。javascript
Javascript是一門單線程語言。相信應該有很多朋友對於Javascript是單線程語言還有些疑問(題外話:以前在某次面試中遇到一個面試官,一來就是「咱們知道JS是一門多線程語言。。。」巴拉巴拉,當時就把我給愣住了。),不是有Web Worker能夠建立多個線程嗎?答案就是,Javascript是單線程的,可是他的運行環境不是單線程。要如何理解這句話,首先得從Javascript運行環境好比瀏覽器的多線程提及。 html
瀏覽器一般包含如下線程:java
Web Worker是瀏覽器爲Javascript提供的一個能夠在瀏覽器後臺開啓一個新的線程的API(相似上面說到瀏覽器的多個線程),使Javascript能夠在瀏覽器環境中多線程運行,但這個多線程是指瀏覽器自己,是它在負責調度管理Javascript代碼,讓他們在恰當時機執行。因此Javascript自己是不支持多線程的。 ios
Javascript的異步過程一般是這樣的:web
這個過程有個問題,異步任務各任務的執行時間過程長短不一樣,執行完成的時間點也不一樣,主線程如何調控異步任務呢?這就引入了消息隊列。面試
棧:函數調用造成的一個由若干幀組成的棧。ajax
堆:對象被分配在堆中,堆是一個用來表示一大塊(一般是非結構化的)內存區域。axios
消息隊列:一個Javascript運行時包含了一個待處理消息的消息隊列。每個消息都關聯着一個用來處理這個消息的回調函數。在事件循環期間,運行時會從最早進入隊列的消息開始處理,被處理的消息會被移出隊列,並做爲輸入參數來調用與之關聯的函數。而後事件循環在處理隊列中的下一個消息。api
瞭解了上述要點,如今回到主題事件循環。那麼Event loop究竟是什麼呢?promise
Event loop是一個執行模型,在不一樣的地方有不一樣的實現。瀏覽器和NodeJS基於不一樣的技術實現了各自的Event loop。
如今明白爲何要把NodeJS排除在外了吧?一樣網上不少Event loop的相關博文一來就是Javascript的Event loop,實際上說的都是瀏覽器的Event loop。
瀏覽器的Event loop是在Html5規範中定義的,大體總結以下:
一個事件循環裏有不少個任務隊列(task queues)來自不一樣任務源,每個任務隊列裏的任務(task)都是嚴格按照先進先出的順序執行的,可是不一樣任務隊列的任務執行順序是不肯定的,瀏覽器會本身調度不一樣任務隊列。也有地方把task稱之爲macrotask(宏任務)。
規範中還提到了microtask(微任務)的概念,如下是規範闡述的進程模型:
執行進入microtask檢查點時,用戶代理會執行如下步驟:
由上可總結爲:在事件循環中,用戶代理會不斷從task隊列中按順序取task執行,每執行完一個task都會檢查microtask隊列是否爲空(執行完一個task的具體標誌是函數執行棧爲空),若是不爲空則會一次性執行完全部microtask。而後再進入下一個循環去task隊列中取下一個task執行。
來看一個例子:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
運行結果是:
script start script end promise1 promise2 setTimeout
那麼問題來了,不是說每一個事件循環開始會從task隊列取最早進入的task執行,而後再執行全部microtask嗎?爲何setTimeout是task卻在Promise.then這個task的前面呢?反正我一開始是有這個疑惑的,不少文章都沒有說清楚這個具體執行的順序,大部分都是在描述規範的時候說的是「每一個事件循環開始會從task隊列中取一個task執行,而後再執行全部microtask」,可是也有部分文章說的是「每一個事件循環開始都是先執行全部microtask」。通過本人多方查證,規範裏的描述如上確實就是每一個事件循環都是先執行task,那爲何上面例子裏面體現出來的是先執行全部microtask呢?
script(總體代碼)屬於task。
來看一下上面例子的詳細執行過程:
這樣是否是清楚了?因此實際上一開始執行script代碼的時候就已經開始事件循環了,這就解釋了爲何好像每次都是先執行全部的microtask。同時,這個例子中還引伸出一個要點:在執行microtask任務的時候,若是又產生了新的microtask,那麼會繼續添加到隊列的末尾,且也會在這個事件循環週期執行,直到microtask隊列爲空爲止。