JavaScript:事件循環機制-宏任務微任務

前面的話

本文將詳細介紹javascript中的事件循環event-loop,目標是讓你徹底弄懂JavaScript的執行機制。


不論是在你面試求職,或是日常開發工作中,我們經常會遇到這樣的情況:給定的幾行代碼,我們需要知道其輸出內容和順序。因爲javascript是一門單線程語言,所以我們可以得出結論:

  • javascript是按照語句出現的順序執行的

那麼我們以爲的JS代碼可能是長這樣的:

let a = '我是第一';
console.log(a);

let b = '我是第二';
console.log(b);

然而實際開發中js是這樣的:

setTimeout(function(){
    console.log('開始定時器')
});

new Promise(function(resolve){
    console.log('創建promise啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('執行then方法咯')
});

console.log('代碼全都執行完啦');

依照JS是按照代碼順序執行這個理念,你可能會自信的寫下輸出結果:

//"開始定時器"
//"創建promise啦"
//"執行then方法咯"
//"代碼全都執行完啦"

結果丟到chrome去驗證下,結果完全不對,瞬間懵了,說好的一行一行執行的呢?

執行結果如下:

// 創建promise啦
// 代碼全都執行完啦
// 執行then方法咯
// 開始定時器

 

所以爲了日後的開發和麪試,我們真的要徹底弄明白JavaScript的執行機制了。

1、關於JavaScript

javascript是單線程的語言,也就是說,同一個時間只能做一件事。而這個單線程的特性,與它的用途有關,作爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程爲準?

2、線程

爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單線程的本質。

排隊執行

單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。但是在處理網絡請求時這是不合適的。因爲一個網絡請求的資源什麼時候返回是不可預知的,這種情況再排隊等待就不明智了。

3、同步和異步

所以,人們將任務分成了同步任務和異步任務。當我們打開網站時,網頁的渲染過程就是一大堆同步任務,比如頁面骨架和頁面元素的渲染。而像加載圖片音樂之類佔用資源大耗時久的任務,就是異步任務。

同步

如果在函數返回的時候,調用者就能夠馬上得到預期結果(即拿到了預期的返回值或者看到了預期的效果),那麼這個函數就是同步的。

Math.abs(-5);
console.log('hello world');

在第一個函數返回時,就拿到了預期的返回值:-5 的絕對值;

在第二個函數返回時候,就看到了預期的效果:在控制檯打印了一個字符串,所以這兩個函數都是同步的。

異步

如果在函數返回的時候,調用者還不能夠馬上得到預期的結果,而是需要在將來通過一定的手段得到,那麼這個函數就是異步的。一般通過傳入回調函數來得到異步函數返回結果。

axios({
      method: "get",
      url: `url`
    }).then(resp => {
        console.log("請求成功", resp);
      }.catch(resp => {
        console.log("請求失敗", resp);
      });

在上面代碼中我們通過axios發送網絡請求,不會馬上得到我們期望的結果,只有在網絡請求完成後,我們才能打印出結果,所以axios函數是異步的。

正是由於JavaScript是單線程的,而異步容易實現非阻塞,所以在JavaScript中對於耗時的操作或者時間不確定的操作,使用異步就成了必然的選擇。

對於同步任務來說,按順序執行即可;但是,對於異步任務,各任務執行的時間長短不同,執行完成的時間點也不同,主線程如何調控異步任務呢?這就用到了消息隊列。

3、消息隊列

消息隊列有很多種叫法,任務隊列,或者叫事件隊列,總之它是一個和異步任務相關的隊列。

可以確定的是,它是隊列這種先入先出的數據結構,和排隊是類似的,哪個異步任務完成的早,就排在前面。不論異步操作何時開始執行,只要異步操作執行完成,就可以到消息隊列中排隊這樣,主線程在空閒的時候,就可以從消息隊列中獲取消息並執行。

消息隊列中放的消息具體是什麼東西?消息的具體結構當然跟具體的實現有關。但是爲了簡單起見,可以認爲:消息就是註冊異步任務時添加的回調函數。

可視化描述

人們把javascript調控同步和異步任務的機制稱爲事件循環,首先來看事件循環機制的可視化描述

 棧

函數調用形成了一個棧幀

function foo(b) {
  var a = 10;
  return a + b + 11;
}
function bar(x) {
  var y = 3;
  return foo(x * y);
}
console.log(bar(7));

當調用bar時,創建了第一個幀 ,幀中包含了bar的參數和局部變量。當bar調用foo時,第二個幀就被創建,並被壓到第一個幀之上,幀中包含了foo的參數和局部變量。當foo返回時,最上層的幀就被彈出棧(剩下bar函數的調用幀 )。當bar返回的時候,棧就空了。

對象被分配在一個堆中,即用以表示一個大部分非結構化的內存區域。

隊列

一個 JavaScript 運行時包含了一個待處理的消息隊列。每一個消息都與一個函數相關聯。當棧擁有足夠內存時,從隊列中取出一個消息進行處理。這個處理過程包含了調用與這個消息相關聯的函數(以及因而創建了一個初始堆棧幀)。當棧再次爲空的時候,也就意味着消息處理結束。

同步異步任務的執行機制

上述流程圖用文字表述是這樣的:

  • 同步和異步任務分別進入不同的執行"場所",同步的進入主線程,異步的進入Event Table並註冊函數。
  • 當指定的事情完成時,Event Table會將這個函數移入Event Queue()。
  • 主線程內的任務執行完畢爲空,會去Event Queue讀取對應的函數,進入主線程執行。
  • 上述過程會不斷重複,也就是常說的Event Loop(事件循環)。

通過對線程同步異步消息隊列的簡單瞭解,我們對JS的執行機制有了一定的認識。根據上圖的認識,我們不禁要問了,那怎麼知道主線程執行棧爲空啊?js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。

接下來就可以詳細的講述事件循環了。

4、事件循環

下面來詳細介紹事件循環。下圖中,主線程運行的時候,產生堆和棧,棧中的代碼調用各種外部API,異步操作執行完成後,就在消息隊列中排隊。只要棧中的代碼執行完畢,主線程就會去讀取消息隊列,依次執行那些異步任務所對應的回調函數。

詳細步驟如下:

  1. 所有同步任務都在主線程上執行,形成一個執行棧.
  2. 主線程之外,還存在一個"消息隊列"。只要異步操作執行完成,就到消息隊列中排隊。
  3. 一旦執行棧中的所有同步任務執行完畢,系統就會按次序讀取消息隊列中的異步任務,於是被讀取的異步任務結束等待狀態,進入執行棧,開始執行。
  4. 主線程不斷重複上面的第三步。

循環

從代碼執行順序的角度來看,程序最開始是按代碼順序執行代碼的,遇到同步任務,立刻執行。

遇到異步任務,則只是調用異步函數發起異步請求。此時,異步任務開始執行異步操作,執行完成後到消息隊列中排隊。

程序按照代碼順序執行完畢後,查詢消息隊列中是否有等待的消息。如果有,則按照次序從消息隊列中把消息放到執行棧中執行。執行完畢後,再從消息隊列中獲取消息,再執行,不斷重複。

由於主線程不斷的重複獲得消息、執行消息、再取消息、再執行。所以,這種機制被稱爲事件循環。用代碼表示大概是這樣:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

如果當前沒有任何消息queue.waitForMessage 會等待同步消息到達。

事件

爲什麼叫事件循環?而不叫任務循環或消息循環。究其原因是消息隊列中的每條消息實際上都對應着一個事件,DOM操作對應的是DOM事件,資源加載操作對應的是加載事件,而定時器操作可以看做對應一個“時間到了”的事件。

5、宏任務和微任務

簡單的來說,微任務和宏任務皆爲異步任務,但是微任務的優先級高於宏任務。

宏任務類型(macro-task):包括整體代碼script,setTimeout,setInterval

微任務類型(micro-task):

 

 不同類型的任務會進入對應的Event Queue,比如setTimeoutsetInterval會進入相同的Event Queue。

事件循環的順序,決定js代碼的執行順序。進入整體代碼(宏任務)後,開始第一次循環。接着執行所有的微任務。然後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行所有的微任務。聽起來有點繞,我們用本系列文章最開始的一段代碼說明:

setTimeout(function(){
    console.log('開始定時器')
});

new Promise(function(resolve){
    console.log('創建promise啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('執行then方法咯')
});

console.log('代碼全都執行完啦');
  • 這段代碼作爲宏任務,進入主線程。
  • 先遇到setTimeout,那麼將其回調函數註冊後分發到宏任務Event Queue。(註冊過程與上同,下文不再描述)
  • 接下來遇到了Promisenew Promise立即執行,打印文字,then函數分發到微任務Event Queue
  • 遇到console.log(),立即執行,打印文字。
  • 好啦,整體代碼script作爲第一個宏任務執行結束,看看有哪些微任務?我們發現了在微任務Event Queue裏面有Promise.then方法,執行。
  • ok,第一輪事件循環結束了,我們開始第二輪循環,當然要從宏任務Event Queue開始。我們發現了宏任務Event Queue中setTimeout對應的回調函數,立即執行。
  • 結束。

6、寫在最後

js的異步

我們從最開頭就說javascript是一門單線程語言,不管是什麼新框架新語法糖實現的所謂異步,其實都是用同步的方法去模擬的,牢牢把握住單線程這點非常重要。

事件循環Event Loop

事件循環是js實現異步的一種方法,也是js的執行機制。

JavaScript的執行和運行

執行和運行有很大的區別,javascript在不同的環境下,比如node,瀏覽器,Ringo等等,執行方式是不同的。而運行大多指javascript解析引擎,是統一的。

setImmediate

微任務和宏任務還有很多種類,比如setImmediate等等,執行都是有共同點的,有興趣的同學可以自行了解。

最後的最後

  • javascript是一門單線程語言
  • Event Loop是javascript的執行機制

牢牢把握兩個基本點,以認真學習javascript爲中心,早日實現成爲前端高手的偉大夢想!

參考資料:

這一次,徹底弄懂 JavaScript 執行機制

深入理解javascript中的事件循環event-loop

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章