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');