理清瀏覽器下的事件循環機制(Event Loop)

咱們知道,JavaScript做爲瀏覽器的腳本語言,起初是爲了與用戶交互和操做DOM,爲了不由於同時操做了同一DOM節點而引發衝突,被設計成爲一種單線程語言。

而單線程語言最大的特性就是同一時間只能作一件事,這個任務未完成下一個任務就要等待,這樣無疑是對資源的極大浪費,並且嚴重時會引發阻塞,形成用戶體驗極差。這個時候就引出了異步的概念,而異步的核心就是事件循環機制Event Loop。node

何爲事件循環機制?

JavaScript的任務分兩種,分別是同步任務和異步任務。segmentfault

  • 同步任務:在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;
  • 異步任務:不進入主線程而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程某個異步任務能夠執行了,該任務纔會進入主線程執行。

clipboard.png

如上圖所示:promise

  1. 主線程在執行代碼的時候,遇到異步任務進入Event Table並註冊回調函數,有了運行結果後將它添加到事件隊列(callback queue)中,而後繼續執行下面的代碼,直到同步代碼執行完。
  2. 主線程執行完同步代碼後,讀取callback queue中的任務,若是有可執行任務則進入主線程執行

不斷重複以上步驟,就造成了事件循環(Event Loop)瀏覽器

clipboard.png

<script>
console.log('start')
setTimeout(function () {
  console.log('setTimeout')
}, 0)
  
console.log('end')
</script>

結合上面步驟分析下這個例子:異步

1. 執行主線程同步任務,輸出start【1】,繼續往下執行
2. 遇到setTimeout,進入event table註冊setTimeout回調,setTimeout回調執行完後,繼續往下執行
3. 輸出end【2】,同步任務執行完畢
4. 進入event queue,檢查是否有可執行任務,取出event queue中setTimeout任務開始執行,輸出setTimeout【3】

結果依次爲:start -> end -> setTimeoutasync

瀏覽器環境下的異步任務

在瀏覽器和node中的事件循環與執行機制是不一樣的,要注意區分,不要搞混。

執行過程

瀏覽器環境的異步任務分爲宏任務(macroTask)和微任務(microtask),當知足條件時會分別被放進宏任務隊列和微任務隊列(先進先出),等待被執行。函數

  • 微任務:
    promise,MutationObserver
  • 宏任務:
    script總體,setTimeout & setIntervat,I/O,UI render。

執行過程以下:oop

clipboard.png

如圖所示:post

1. 把總體的script代碼做爲宏任務執行
2. 執行過程當中若是遇到宏任務和微任務,知足條件時分別添加至宏任務隊列和微任務隊列
3. 執行完一個宏任務後,取出全部微任務依次執行,若是微任務一直有新的被添加進來,則一直執行,直到把微任務隊列清空
4. 不斷重複2和3,直到全部任務被清空,結束執行。

clipboard.png

<script>
console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(() => {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(() => {
    console.log('promise2')
  })
}, 0)
setTimeout(() => {
  console.log('timer3')
  Promise.resolve().then(() => {
    console.log('promise3')
  })
}, 0)
new Promise(function(resolve) {
    console.log('promise4');
    resolve();
}).then(function() {
    console.log('promise5')
})

console.log('end')
</script>

分析:測試

  • 第一輪:

    1. 輸出start【1】,將setTimeout回調函數@1,放進宏任務隊列;
    2. 將setTimeout回調函數@2,放進宏任務隊列;
    3. 將setTimeout回調函數@3,放進宏任務隊列;
    4. 執行new Promise函數輸出promise4【2】,將Promise.then@1放進微任務隊列;
    5. 輸出end【3】,此時隊列以下所示:
      clipboard.png
    6. 第一輪宏任務執行完畢,開始執行微任務,取出微任務Promise.then@1,輸出promise5【4】,此時微任務隊列被清空,開始第二輪執行。
  • 第二輪:

    1. 取出宏任務setTimeout回調函數@1,輸出timer1【5】,將回調函數中的Promise.then@2放進微任務隊列;
    2. 宏任務setTimeout回調函數@1中無宏任務,開始執行微任務,取出Promise.then@2,輸出promise1【6】,此時:

      clipboard.png

    3. setTimeout回調函數@1中宏任務隊列和微任務隊列均被清空,開始第三輪執行
  • 第三輪:

    1. 取出宏任務setTimeout回調函數@2,輸出timer2【7】,將Promise.then@3放進微任務隊列;
    2. setTimeout回調函數@2中無宏任務,開始執行微任務,取出Promise.then@3,輸出promise2【8】,此時:

      clipboard.png

    3. 宏任務setTimeout回調函數@2中宏任務隊列和微任務隊列均被清空,開始第四輪執行
  • 第四輪:

    1. 取出宏任務setTimeout回調函數@3,輸出timer3【9】,將Promise.then@4放進微任務隊列;
    2. setTimeout回調函數@3中無宏任務,開始執行微任務,取出Promise.then@4,輸出promise3【10】

如今宏任務對列和微任務隊列都被清空了,完成執行,結果爲:start > promise4 > end > promise5 > timer1 > promise1 > timer2 > promise2 > timer3 > promise3

引入 async/await

asnyc知識點傳送門

await表達式的運算結果取決於它右側的結果

當遇到await時,會阻塞函數體內部處於await後面的代碼,跳出去執行該函數外部的同步代碼,當外部同步代碼執行完畢,再回到該函數內部執行剩餘的代碼

補充aynsc的一點知識:

若是aynsc函數中return一個直接量,async 會把這個直接量經過Promise.resolve()封裝成Promise對象,若是什麼都沒return,會被封裝成Promise.resolve(undefined)

那麼 引入了async await以後的執行過程是怎樣的呢?

clipboard.png

<script>
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("setTimeout");
}, 0);

async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});

console.log("script end");
</script>

分析:

  • 第一輪:

    1. 執行同步代碼,輸出:script start【1】,將setTimeout回調@1放入宏任務隊列;
    2. 進入aynsc1函數中,執行同步代碼輸出:async1 start【2】,遇到await從右向左執行,進入async2函數,輸出:async2【3】;aynsc2函數體中未返回任何東西等價於返回了Promise.resolve(undefined),拿到返回值後進入aynsc1函數體中,繼續執行剩下的部分,這時候aynsc1中註釋部分等價於:

      async function async1() {
        console.log("async1 start");
        //await async2();
        //console.log("async1 end");
         await new Promise((resolve) => resolve()).then(resolve => {
           console.log('async1 end')
         })
      }

      將Promise.then@1推入到微任務隊列;

    3. 繼續執行同步代碼,輸出:promise1【4】,將Promise.then@2推入微任務隊列
    4. 繼續執行同步代碼,輸出:script end【5】,第一輪宏隊列任務執行完畢,此時以下:

      clipboard.png

    5. 開始執行微任務,取出微任務Promise.then@1,值爲undefined,這個時候Promise.then@1完成執行,則await aynsc2()獲得了值也完成了執行,再也不阻塞後面代碼,那麼執行同步代碼輸出:async1 end【6】;
    6. 取出微任務Promise.then@2,輸出:promise2【7】,微任務所有執行完畢,如今開始第二輪執行
  • 第二輪:

    1. 取出宏任務隊列中的setTimeout@1,輸出setTimeout【8】

全部任務隊列均爲空,結束執行,輸出結果爲:script start > async1 start > async2 > promise1 > script end > async1 end > promise2 > setTimeout

補充谷歌瀏覽器測試結果:

clipboard.png

clipboard.png 借用一個例子:await一個直接值的狀況

<script>
console.log('1')
async function async1() {
  console.log('2')
  await 'await的結果'
  console.log('5')
}

async1()
console.log('3')

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('6')
})
</script>

分析:

  • 第一輪:

    1. 執行同步函數,輸出:1【1】,進入async1函數中,輸出:2【2】,這個時候await雖然接收了一個直接值,可是仍是要先執行外邊的同步代碼以後才能執行await後邊的值
    2. 繼續執行同步代碼,輸出:3【3】,進入Promise函數,輸出:4【4】,將Promise.then推入微任務隊列
    3. 同步代碼執行完畢,進入 async1函數中輸出:5【5】
    4. 宏任務執行完畢,進入微任務隊列,開始執行微任務;取出Promise.then,輸出:6【6】

任務隊列爲空,執行完畢,結果爲: 1 > 2 > 3 > 4 > 5 > 6

clipboard.png再借個例子,這個有點複雜

<script>
setTimeout(function () {
  console.log('8')
}, 0)

async function async1() {
  console.log('1')
  const data = await async2()
  console.log('6')
  return data
}

async function async2() {
  return new Promise(resolve => {
    console.log('2')
    resolve('async2的結果')
  }).then(data => {
    console.log('4')
    return data
  })
}

async1().then(data => {
  console.log('7')
  console.log(data)
})

new Promise(function (resolve) {
  console.log('3')
  resolve()
}).then(function () {
  console.log('5')
})
</script>

分析:

  • 第一輪:

    1. 將setTimeOut@1放入宏任務列隊;
    2. 執行async1()函數體內的函數,輸出:1【1】,遇到await,進入aynsc2函數體,輸出:2【2】,將該函數體內promise.then@1放入微任務隊列中;
    3. 執行New promise .. 輸出3【3】,將該函數體內Promise.then@2放入微任務隊列中,第一輪宏任務執行完畢,此時:

      clipboard.png

    4. 開始執行第一輪微任務,取出Promise.then@1,輸出:4【4】,此時async2函數執行完畢,進入aynsc1函數,此時改動下aynsc1函數,等價於:

      async function async1() {
        console.log('1')
        //const data = await async2()
        //console.log('6')
         const data = await new Promise(resolve => resolve('async2的結果')).then((resolve) => {
                          console.log(6); 
                          return resolve;
                      })
      
         return data;
      }

      將上面promise.then@3推入微任務隊列中,此時:

      clipboard.png

    5. 接着執行微任務,取出promise.then@2,輸出:5【5】,取出promise.then@3,輸出:6【6】,此時函數async1執行完成,接着執行async1().then(...),將async1().then@1推到微任務隊列中,取出async1().then@1,輸出:7【7】和 'async2的結果'【8】;
    6. 第一輪任務執行完畢,開始執行第二輪,此時:

      clipboard.png

  • 第二輪:

    1. 開始執行第二輪宏任務,將setTimeOut@1取出執行,輸出8【9】,完畢。

因此任務被執行完畢,結果爲:1 > 2 > 3 > 4 > 5 > 6 > 7 > async2的結果 > 8

------------------------ END ----------------------------

PS: 好記性不如爛筆頭,看了那麼多資料,仍是想總結一下,否則過一陣子就忘記了,若是辛苦指出哦,謝謝~

參考資料:

理解 JavaScript 的 async/await
瀏覽器和Node不一樣的事件循環(Event Loop)
Event Loop 原來是這麼回事
這一次,完全弄懂 JavaScript 執行機制
從event loop到async await來了解事件循環機制...

相關文章
相關標籤/搜索