JavaScript的事件隊列(Event Queue)

前言

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

宏任務和微任務

首先咱們先來看一段代碼:異步

<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> 複製代碼

這些日誌的打印順序是:async

Start
Promise
End
Then
SetTimeout
複製代碼

這是爲何函數

首先,咱們知道JavaScript的一大特色就是單線程,而這個線程中擁有惟一的一個事件循環。oop

一個線程中,事件循環是惟一的,可是任務隊列能夠擁有多個。ui

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

宏任務線程

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

微任務日誌

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

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

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

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

因此在上面的代碼中宏任務有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 1
  • 繼續向下執行,又遇到一個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() 遇到Promise,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;

  • 遇到Promise,new Promise直接執行。then後面的函數被分發到微任務Event Queue中

    宏任務 微任務
    setTimeout1 new Promise() => 後的then1
  • 再次遇到Promise,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. 當前宏任務執行完畢,當即執行微任務隊列中的任務(微任務存在優先級,優先級高的先執行)。
  4. 當前微任務隊列中的任務執行完畢,檢查渲染,GUI線程接管渲染。
  5. 繼續執行下一個宏任務從事件隊列中取。

因此在咱們寫下setTimeout(fn,0)的時候他並非在當時當即執行,是從下一個Event loop開始執行,便是等當前全部腳本執行完再運行,就是"儘量早"。

相關文章
相關標籤/搜索