JavaScript進階之路系列(二): 事件循環
- 2020 年 4 月 8 日
- 筆記
我們面試的時候經常會問到事件循環,也就是event loop。很多時候我們都是一臉懵,我們通常會背關於事件循環的面試題,講給面試官的時候自己都不知道自己在講什麼,可能面試官也不太了解事件循環,只是看別人都這麼問。那麼,仔細了解一下事件循環吧,對以後的編程真的會有幫助的。
1.為什麼js是單線程?
js作為主要運行在瀏覽器的腳本語言,js主要用途之一是操作DOM。
舉一個例子,如果js同時有兩個線程,同時對同一個dom進行操作,這時瀏覽器應該聽哪個線程的,如何判斷優先級?
為了避免這種問題,js必須是一門單線程語言,並且在未來這個特點也不會改變。
單線程就意味着,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。
var i, t = Date.now() for (i = 0; i < 100000000; i++) {} console.log(Date.now() - t) // 238
像上面這樣,如果排隊是因為計算量大,CPU忙不過來,但是,如果是網絡請求就不合適。因為一個網絡請求的資源什麼時候返回是不可預知的,這種情況再排隊等待就不明智了。
所以出現了同步與異步。
2.同步和異步
同步
同步任務是指在主線程上排隊執行的任務,只有前一個任務執行完畢,才能繼續執行下一個任務。
// 同步代碼 function fun1() { console.log(1); } function fun2() { console.log(2); } fun1(); fun2();
輸出會依次輸入1,2,因為代碼是從上到下依次執行,執行完fun1(),才繼續執行fun2()。
異步
異步任務是指不進入主線程,而進入任務隊列的任務,只有任務隊列通知主線程,某個異步任務可以執行了,該任務才會進入主線程。
function fun1() { console.log(1); } function fun2() { console.log(2); } function fun3() { console.log(3); } fun1(); setTimeout(function(){ fun2(); },0); fun3();
依次輸出1,3,2,因為我們會優先執行同步函數,然後在執行異步函數。
正是由於JavaScript是單線程的,而異步容易實現非阻塞,所以在JavaScript中對於耗時的操作或者時間不確定的操作,使用異步就成了必然的選擇。 JavaScript的執行順序:(重點)
1.先同步後異步。 2.異步中任務隊列的執行順序: 先微任務microtask隊列,再宏任務macrotask隊列。 3.調用Promise 中的resolve,reject屬於微任務隊列,setTimeout屬於宏任務隊列。
那什麼是微任務和宏任務?
3.宏任務與微任務
異步任務分為 宏任務(macrotask) 與 微任務 (microtask),不同的API註冊的任務會依次進入自身對應的隊列中,然後等待 Event Loop 將它們依次壓入執行棧中執行。
宏任務
macrotask,可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)。
瀏覽器為了能夠使得JS內部(macro)task與DOM任務能夠有序的執行,會在一個macrotask執行結束後,在下一個macrotask 執行開始前,對頁面進行重新渲染,流程如下:
macrotask->渲染->macrotask->...
宏任務包含:
script(整體代碼) setTimeout setInterval I/O UI交互事件 postMessage MessageChannel setImmediate(Node.js 環境)
微任務
microtask,可以理解是在當前 task 執行結束後立即執行的任務。也就是說,在當前task任務後,下一個task之前,在渲染之前。
所以它的響應速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染。也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)。
微任務包含:
Promise.then Object.observe MutaionObserver process.nextTick(Node.js 環境)
4.Event Loop(事件循環)
Event Loop(事件循環)中,每一次循環稱為 tick, 每一次tick的任務如下:
1.執行棧選擇最先進入隊列的宏任務(通常是script整體代碼),如果有則執行。 2.檢查是否存在 Microtask,如果存在則不停的執行,直至清空 microtask 隊列。 3.更新render(每一次事件循環,瀏覽器都可能會去更新渲染)。 4.重複以上步驟。
宏任務 > 所有微任務 > 宏任務,如下圖所示:

從代碼執行順序的角度來看,程序最開始是按代碼順序執行代碼的,遇到同步任務,立刻執行;遇到異步任務,則只是調用異步函數發起異步請求。此時,異步任務開始執行異步操作,執行完成後到消息隊列中排隊。程序按照代碼順序執行完畢後,查詢消息隊列中是否有等待的消息。如果有,則按照次序從消息隊列中把消息放到執行棧中執行。執行完畢後,再從消息隊列中獲取消息,再執行,不斷重複。
由於主線程不斷的重複獲得消息、執行消息、再取消息、再執行。所以,這種機制被稱為事件循環。 用代碼表示:
while (queue.waitForMessage()) { queue.processNextMessage(); }
如果當前沒有任何消息queue.waitForMessage 會等待同步消息到達。
5.實例
下面以一個實例來解釋事件循環機制:
console.log(1) div.onclick = () => {console.log('click')} console.log(2) setTimeout(() => {console.log('timeout')},1000)
1、執行第一行代碼,第一行是一個同步任務,控制台顯示1
2、執行第二行代碼,第二行是一個異步任務,發起異步請求,可以在任意時刻執行鼠標點擊的異步操作
3、執行第三行代碼,第三行是一個同步任務,控制台顯示2
4、執行第四行代碼,第四行是一個異步任務,發起異步請求,1s後執行定時器任務
5、假設從執行第四行代碼的1s內,執行了鼠標點擊,則鼠標任務在消息隊列中排到首位
6、從執行第四行代碼1s後,定時器任務到消息隊列中排到第二位
7、現在同步任務已經執行完畢,則從消息隊列中按照次序把異步任務放到執行棧中執行
8、則控制台依次顯示』click『、『timeout』
9、過了一段時間後,又執行了一次鼠標點擊,由於消息隊列中已經空了,則鼠標任務在消息隊列中排到首位
10、同步任務執行完畢後,再從消息隊列中按照次序把異步任務放到執行棧中執行
11、 則控制台顯示』click』
異步過程
下面以一個實例來解釋一次完整的異步過程:
div.onclick = function fn(){console.log('click')}
1、主線程通過調用異步函數div.onclick發起異步請求
2、在某一時刻,執行異步操作,即鼠標點擊
3、接着,回調函數fn到消息隊列中排隊
4、主線程從消息隊列中讀取fn到執行棧中
5、然後在執行棧中執行fn裏面的代碼console.log(『click』)
6、於是,控制台顯示』click』
例題: 依次輸出什麼?
function fun1() { console.log("1") } fun1() setTimeout(function () { console.log('2') }); function fun3() { console.log("3") } fun3() var l4 = new Promise(function (resolve) { for (var i = 0; i < 10000; i++) { i == 99 && resolve(); } }).then(function () { console.log('4') }); console.log('5');