JavaScript進階之路系列(二): 事件循環

我們面試的時候經常會問到事件循環,也就是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');