事件迴圈

冰凉小手發表於2024-03-20

JS為什麼是單執行緒?

JS最初設計是應用在瀏覽器中執行,假如JS是多執行緒機制,則可能存在這樣的情況:

現有兩個執行緒Process1和Process2,它們同時對同一個Dom節點進行操作,其中Process1刪除該Dom節點,而Process2編輯該Dom節點。這是兩個矛盾的命令,瀏覽器將無法執行。複製程式碼

因此,JS需要被設計成單執行緒

JS為什麼需要非同步?

由於JS的單執行緒機制,決定了其執行順序自上而下。如果不存在非同步,則後邊的程式必須等待前邊的程式執行完成才可以進行開始執行。如果前邊的程式程式執行時間過長,則導致執行緒阻塞,瀏覽器可能處在長時間無響應的狀態。因此需要非同步執行。

JS如何實現非同步?

JS是通過事件迴圈(event loop)來實現非同步的,event loop的機制代表了JS的執行機制。

舉個例子:

console.log(1);
setTimeout(() => {
    console.log(2);
}, 0);
console.log(3);複製程式碼

以上程式的執行結果是:1,3,2。

也就是說,setTimeout裡的函式並沒有立即執行,而是延遲了一段時間,在滿足一定的條件之後才會執行。這樣的程式碼稱為非同步程式碼,反之稱為同步程式碼。

在此可以簡單概括JS的執行機制如下(event loop(1)):

  • 首先判斷JS是同步還是非同步, 同步任務立即進入主執行緒,非同步任務則進入到event table
  • 非同步任務在event table中註冊函式,當滿足觸發條件之後,該任務被推入到event queue
  • 同步任務會在主執行緒上一直執行,直到主執行緒處於空閒狀態,此時,主執行緒會到event quene中檢視是否有可執行的任務,如有,則將該任務推入主執行緒中繼續執行

如此反覆,稱為事件迴圈

在此對以上例子進行解析:

console.log(1); //任務1,同步任務,進入到主執行緒裡
setTimeout(() => { //任務2,非同步任務,進入到event table註冊函式,0秒之後被推入event queue中
    console.log(2);
}, 0); 
console.log(3); //任務3,同步程式,進入到主執行緒裡

主執行緒在完成了任務1、任務3後,檢查event queue是否存在可執行函式,執行setTimeout裡的函式。

因此最終的輸出結果是1-3-2。複製程式碼

在此需要注意的是,非同步任務的執行需要兩個條件:

  • 滿足觸發條件
  • 主執行緒空閒

因此,將函式體setTimeout(() => fn(), 3000)解釋為“定時器在3秒之後執行fn”並不準確,準確的解釋應該是:

3秒後,fn被推入到event queue,當主執行緒空閒時,fn從event quene推入到主執行緒中執行複製程式碼

正因如此,我們並不能完全依賴setTimeout作為一個定時器,對於setTimeout(() => fn(), 3000),如果主執行緒需要執行10秒,則fn實際上是13秒後才開始執行。

再看一個例子:

setTimeout(() => console.log(1), 0);

new Promise(resolve => {
    resolve(2);
    console.log(3);
}).then((res) => {
    console.log(res);
});

console.log(4);複製程式碼

假如我們利用之前的知識去分析:

setTimeout(() => console.log(1), 0); //任務1,非同步任務,進入event table註冊,0秒後進入event queue

new Promise(resolve => { //任務2,同步任務,其中包含
    resolve(2);
    console.log(3); //任務3,同步任務
}).then((res) => { //任務4,非同步任務,在event table註冊後進入event queue,排在任務1之後
    console.log(res); 
});

console.log(4); //任務5,同步任務複製程式碼

根據此分析,最終的輸出結果是:3-4-1-2

這是正確的輸出結果嗎?程式執行之後,得到的最終結果應該是:3-4-2-1

是否因為非同步任務的執行順序不是前後順序而另有規定,導致輸出結果與我們預知的不一樣?

事實上,單純的按照非同步和同步的劃分方式,並不準確。

準確的劃分方式是:

  • macro-task(巨集任務):包括整體程式碼script,setTimeout,setInterval
  • micro-task(微任務):Promise, process.nextTick

MacDown Screenshot

按照這樣的分類方式,JS的執行機制是(event loop(2)):

  • 執行一個巨集任務,過程中如果遇到微任務,就將其放在微任務的【事件佇列】裡
  • 當前巨集任務執行完成後,會檢視微任務的【事件佇列】,並將其中的全部微任務執行完成

重複以上2步驟,結合event loop(1)和event loop(2),就可以得到更準確的JS執行機制。

此時我們再去分析剛剛出錯的列子:

1.首先執行script下的巨集任務,遇到setTimeout,將其放在巨集任務的【佇列】裡
2.遇到 new Promise直接執行,裡邊的同步任務cosnole.log(3)立即觸發
3.遇到 then 方法,是微任務,將其放在微任務的【佇列】裡
4.遇到console.log(4)直接執行。
5.當主執行緒完成cosnole.log(3)和console.log(4)後,會去檢查微任務的【佇列】,發現其中的任務 then,於是執行console.log(res),此處的res === 2;
6.當微任務完成之後,會去檢查巨集任務【佇列】,發現setTimeout,並執行複製程式碼


相關文章