一文告訴你 Event Loop 是什么?


 

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。主線程對任務隊列的讀取也有先后之分,首先會去查找微任務,微任務隊列的事件都執行完畢后,再讀取最前面的宏任務進行執行,執行完再讀取微任務隊列,這個過程也是循環往復的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM