JavaScript的事件隊列(Event Queue)---宏任務和微任務-轉載

前言

在寫代碼的時候經常思考一個問題,到底是那個函數先執行,本身JavaScript是一門單線程的語言,意思就是按照順序執行。但是加入一些setTimeout和promise的函數來又實現了異步操作,常常我會寫一個setTimeout(fn,0),他會立即執行嗎?

宏任務和微任務

首先我們先來看一段代碼:

<script>
console.log("Start");

setTimeout(function(){
console.log("SetTimeout");
},0);

new Promise(function(resolve,reject){
console.log("Promise");
resolve();
}).then(function(){
console.log("Then");
});

console.log("End");
<script>

這些日誌的打印順序是:

Start
Promise
End
Then
SetTimeout

這是爲什麼

首先,我們知道JavaScript的一大特點就是單線程,而這個線程中擁有唯一的一個事件循環。

一個線程中,事件循環是唯一的,但是任務隊列可以擁有多個。

任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。

宏任務

  • setTimeout
  • setInterval
  • I/O
  • script代碼塊

微任務

  • nextTick
  • callback
  • Promise
  • process.nextTick
  • Object.observe
  • MutationObserver

事件循環的順序,決定js代碼的執行順序。一段代碼塊就是一個宏任務。進入整體代碼(宏任務)後,開始第一次循環。接着執行所有的微任務。然後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行所有的微任務。

主線程(宏任務) => 微任務 => 宏任務 => 主線程

下圖是簡易版的事件循環:

 

所以在上面的代碼中宏任務有script代碼塊,setTimeout,微任務有Promise

事件循環流程分析如下:

  • 整體script 作爲第一個宏任務進入主線程,遇到console.log,輸出Start
  • 遇到setTimeout,其回調函數被分發到宏任務Event Queue中。
  • 遇到Promise,new Promise直接執行,輸出Promise。then被分發到微任務Event Queue中。
  • 遇到console.log,立即執行,輸出End
  • 整體代碼script作爲第一個宏任務執行結束,看看有哪些微任務?我們發現了then在微任務Event Queue裏面,執行
  • ok,第一輪事件循環結束了,我們開始第二輪循環,當然要從宏任務Event Queue開始。我們發現了宏任務Event Queue中setTimeout對應的回調函數,立即執行。
  • 所以代碼結束。

提高下難度在來一段較爲複雜的代碼來檢驗是否已經基本瞭解了事件循環的機制

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout1');
}, 200);
setTimeout(function() {
    console.log('setTimeout2');
    new Promise(function(resolve) {
        resolve();
    }).then(function() {
        console.log('then1')
    })
    new Promise(function(resolve) {
        console.log('Promise1');
        resolve();
    }).then(function() {
        console.log('then2')
    })
},0)
async1();
new Promise(function(resolve) {
    console.log('promise2');
    resolve();
  }).then(function() {
    console.log('then3');
  });
console.log('script end');
 

第一輪事件循環流程分析如下:

  • 整體script作爲第一個宏任務進入主線程,async1(),和async12()函數申明,但並沒有執行,遇到console.log輸出script start

  • 繼續向下執行,遇到setTimeout,把它的回調函數放入宏任務Event Queue。(ps:暫且叫他setTimeout1)

    宏任務 微任務
    setTimeout1  
  • 繼續向下執行,又遇到一個setTimeout,繼續將他放入宏任務Event Queue。(ps:暫且叫他setTimeout2)

    宏任務 微任務
    setTimeout1  
    setTimeout2  
  • 遇到執行async1(), 進入async的執行上下文之後,遇到console.log輸出 async1 start

  • 然後遇到await async2(),由於()的優先級高,所有立即執行async2(),進入async2()的執行上下文。

  • 看到console.log輸出async2,之後沒有返回值,結束函數,返回undefined,返回async1的執行上下文的await undefined,由於async函數使用await後得語句會被放入一個回調函數中,所以把下面的放入微任務Event Queue中。

    宏任務 微任務
    setTimeout1 async1 => awati 後面的語句
    setTimeout2  
  • 結束async1() 遇到PromisePromise本身是同步的立即執行函數 new Promise直接執行,輸出Promise2then後面的函數被分發到微任務Event Queue中

    宏任務 微任務
    setTimeout1 async1 => awati 後面的語句
    setTimeout2 new Promise() => 後的then
  • 執行完Promise(),遇到console.log,輸出script end,這裏一個宏任務代碼塊執行完畢。

  • 在主線程執行的過程中,事件觸發線程一直在監聽着異步事件, 當主線程空閒下來後,若微任務隊列中有任務未執行,執行的事件隊列(Event Queue)中有微任務,遇到new Promise()後面的回調函數,執行代碼,輸出then3

  • 看到 async1await後面的回調函數,執行代碼,輸出async1 end(注意:如果倆個微任務的優先級相同那麼任務隊列自上而下執行,但是promise的優先級高於async,所以先執行promise後面的回調函數)

  • 自此,第一輪事件循環正式結束,這一輪的結果是輸出:

    script start => async1 start => async2 => promise2 => script end => then3 => async1 end
    宏任務 微任務
    setTimeout1  
    setTimeout2  
  • 那麼第二輪時間循環從setTimeout宏任務開始:

  • setTimeout和setInterval的運行機制是,將指定的代碼移出本次執行,等到下一輪Event Loop時,再檢查是否到了指定時間。如果到了,就執行對應的代碼;如果不到,就等到再下一輪Event Loop時重新判斷。因爲setTimeout1有200ms的延時,並沒到達指定時間,所以先執行setTimeout2這個宏任務

  • 進入到setTimeout2,遇到console.log首先輸出setTimeout2;

  • 遇到PromisePromise本身是同步的立即執行函數new Promise直接執行。then後面的函數被分發到微任務Event Queue中

    宏任務 微任務
    setTimeout1 new Promise() => 後的then1
       
  • 再次遇到PromisePromise本身是同步的立即執行函數new Promise直接執行輸出promise1then後面的函數被分發到微任務Event Queue中

    宏任務 微任務
    setTimeout1 new Promise() => 後的then1
    new Promise() => 後的then2
  • 主線程執行執行空閒,開始執行微任務隊列中依次輸出then1then2

  • 第二輪事件循環正式結束。第二輪依次輸出

    promise1 => then1 => then2
  • 現在任務隊列中只有個延時200ms的setTimeout1,在到達200ms後執行setTimeout的回調函數輸出setTimeout1

  • 時間循環結束

  • 整段代碼,完整的輸出爲

    script start => async1 start => async2 => promise2 => script end => then3 => async1 end => promise1 => then1 => then2 => setTimeout1

總結

  1. 在執行棧中執行一個宏任務。
  2. 在執行過程中遇到微任務和宏任務,分別添加到微任務隊列和宏任務隊列中去。
  3. 當前宏任務執行完畢,立即執行微任務隊列中的任務(微任務存在優先級,優先級高的先執行(promise的優先級高於async)。
  4. 當前微任務隊列中的任務執行完畢,檢查渲染,GUI線程接管渲染。
  5. 繼續執行下一個宏任務從事件隊列中取。

所以在我們寫下setTimeout(fn,0)的時候他並不是在當時立即執行,是從下一個Event loop開始執行,即是等當前所有腳本執行完再運行,就是"儘可能早"。

轉載於:https://www.cnblogs.com/wangtong111/p/11213524.html

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