Event Loop 也叫做“事件循環”,它其實與 JavaScript 的運行機制有關。
JS初始設計
JavaScript 在設計之初便是單線程,程序運行時,只有一個線程存在,在特定的時候只能有特定的代碼被執行。這和 JavaScript 的用途有關,它是一門瀏覽器腳本語言,通常是用來操作 DOM 的,如果是多線程,一個線程進行了刪除 DOM 操作,另一個添加 DOM,此時該如何處理?所以 JavaScript 在設計之初便是單線程的。
雖然 HTML5 增加了 Web Work 可用來另開一個線程,但是該線程仍受主線程的控制,所以 JavaScript 的本質依然是單線程
線程和進程
進程和線程是操作系統中的概念,在操作系統中,一個任務就是一個進程,比如你在電腦上打開了一個瀏覽器來觀看視頻,便是打開了一個瀏覽器進程,此時又想記錄視頻中的重要信息,於是你打開了備忘錄,這便是一個備忘錄進程,系統會為每個進程分配它所需要的地址空間,數據,代碼等系統資源。如果把一個進程看做一個小的車間,車間里有很多工人,有的負責操作機器,有的負責搬運材料,每個工人可以看做一個線程,線程可以共享進程的資源。可以說,線程是進程的最小單位,一個進程可以包含多個線程。
執行棧和任務隊列
單線程的 JavaScript 一段一段地執行,前面的執行完了,再執行后面的,試想一個,如果前一個任務需要執行很久,比如接口請求、I/O 操作,此時后面的任務只能干巴巴地等待么?干等不僅浪費了資源,而且頁面的交互程度也很差。JavaScript 意識到了這個問題,他們將任務分成了同步任務和異步任務,對於二者有不同的處理。
JavaScript 在運行時會將變量存放在堆(heap)和棧(stack)中,堆中通常存放着一些對象,而變量及對象的指針則存放在棧中。JavaScript 在執行時,同步任務會排好隊,在主線程上按照順序執行,前面的執行完了再執行后面的,排隊的地方叫執行棧(execution context stack)。JavaScript 對異步任務不會停下來等待,而是將其掛起,繼續執行執行棧中的同步任務,當異步任務有返回結果時,異步任務會加入與執行棧不一樣的隊列,即任務隊列(task queue),所以任務隊列中存放的是異步任務執行完成后的結果,通常是回調函數。
當執行棧的同步任務已經執行完成,此時主線程閑下來,它便會去查看任務隊列是否有任務,如果有,主線程會將最先進入任務隊列的任務加入到執行棧中執行,執行棧中的任務執行完了之后,主線程便又去任務隊列中查看是否有任務可執行。主線程去任務隊列讀取任務到執行棧中去執行,這個過程是循環往復的,這便是 Event Loop,事件循環。
網上有張流傳甚廣的圖對這一過程進行了總結,在圖中我們可以看到,JavaScript 在運行時產生了堆和棧,ajax、setTimeout 等異步任務被掛起,異步任務的返回結果加入任務隊列,主線程會循環往復地讀取任務隊列中的任務,加入執行棧中執行。
(JavaScript 運行機制,圖片來源於網絡)
宏任務與微任務
異步任務有更深一層的划分,它們是宏任務(macro task)和微任務(micro task),二者的執行順序也有差別。在上面我們講到異步任務的結果會進入任務隊列中,對於不同的事件類型,宏任務會加入宏任務隊列,微任務會加入微任務隊列。在執行棧中的同步任務執行完成后,主線程會先查看任務隊列中的微任務,如果有沒有,則去宏任務隊列中取出最前面的一個事件加入執行棧中執行;如果有,則將所有在微任務隊列中的事件依次加入執行棧中執行,直到所有事件執行完成后,再去宏任務中取出最前面的一個事件加入執行棧,如此循環往復。
由此我們可以得出結論,主線程總是會先查看微任務隊列,等到微任務隊列中的事件都處理完成后,再去宏任務隊列中添加一個事件到任務棧中執行。
常見的宏任務有 setTimeout,setInterval;常見的微任務有 new Promise。
代碼例子體驗
console.log(1) setTimeout(function() { console.log(2) new Promise(function(resolve) { console.log(3) resolve(4) }).then(function(num) { console.log(num) }) }, 300) new Promise(function(resolve) { console.log(5) resolve(6) }).then(function(num) { console.log(num) }) setTimeout(function() { console.log(7) }, 400)
我們一步步來分析上面的執行順序,當程序開始執行時,首先打印出 1,然后遇到了 setTimeout,主程序將它掛起,300 毫秒后它的回調函數進入宏任務隊列,我們記做 setTimeout1。隨后遇到了 new Promise,resolve 部分是同步執行的,所以會打印出 5,then 中的回調函數進入微任務隊列,我們暫時記做 promise1。最后是 setTimeout,同理在 400 毫秒后加入了宏任務隊列,我們記做 setTimeout2。
此時任務隊列的情況如下:
宏任務 |
微任務 |
setTimeout1 |
promise1 |
setTimeout2 |
執行棧中的同步任務執行完成后,主線程查看任務隊列時發現存在微任務,於是把 promise1 執行了,打印出 6。此時微任務隊列已經空了,於是開始查看宏任務隊列,將 setTimeout1 的回調函數加入任務棧開始執行,於是首先打印出 2,之后是 3,再將 then 中的回調函數加入微任務隊列,我們記做 promise2。
此時任務隊列的情況如下:
宏任務 |
微任務 |
setTimeout2 |
promise2 |
此時執行棧也空了,於是將微任務 promise2 加入執行棧,打印出 4。此時微任務已經執行完,再查看宏任務隊列,於是執行 setTimeout2,打印出 7。
所以代碼中的輸出順序是 1,5,6,2,3,4,7。
注意,主線程對微任務的讀取是逐個讀取,直到微任務隊列為空,再讀取宏隊列,對宏任務隊列的讀取在一個循環中只讀取一個。
小結
我們了解了 JavaScript 的運行機制,它是單線程的。JavaScript 中的任務可分為同步任務和異步任務,同步任務總是先進入執行棧中執行,異步任務會被掛起,直到有結果返回時,異步任務會進入任務隊列中等待主線程讀取執行。當執行棧為空時,主線程便會循環往復地讀取任務隊列中的事件,進入執行棧執行,這個過程叫 Event Loop。主線程對任務隊列的讀取也有先后之分,首先會去查找微任務,微任務隊列的事件都執行完畢后,再讀取最前面的宏任務進行執行,執行完再讀取微任務隊列,這個過程也是循環往復的。