我是這樣理解EventLoop的

我是這樣理解EventLoop的

在這裏插入圖片描述

一、前言

  衆所周知,在使用javascript時,經常需要考慮程序中存在異步的情況,如果對異步考慮不周,很容易在開發中出現技術錯誤和業務錯誤。作爲一名合格的javascript使用者,瞭解異步的存在和運行機制十分重要且有必要;那麼,異步究竟是何方神聖呢?我們不得不提Event Loop:也叫做事件循環,是指瀏覽器或Node環境的一種解決javaScript單線程運行時不會阻塞的一種機制,也就是實現異步的原理。作爲一種單線程語言,javascript本身是沒有異步這一說法的,是由其宿主環境提供的(EventLoop優秀文章網上有很多,這篇文章是自己的整合和理解)。
注意:Event Loop 並不是在 ECMAScript 標準中定義的,而是在 HTML 標準中定義的;

二、Event Loop知識鋪墊

  javascript代碼運行時,任務被分爲兩種,宏任務(MacroTask/Task)微任務(MircoTask)Event Loop在執行和協調各種任務時也將任務隊列分爲Task QueueMircoTak Queue分別對應管理宏任務(MacroTask/Task)微任務(MircoTask);作爲隊列,Task QueueMircoTak Queue也具備隊列特性:先進先出(FIFO—first in first out)

1、微任務(MircoTask)

  在 HTML 標準中,並沒有明確規定 Microtask,但是實際開發中包含以下四種:

  • Promise中的then、catch、finally(原理參考:【js進階】手撕Promise,一碼一解析 包懂
  • MutationObserver(監視 DOM 變動的API,詳情參考MDN
  • Object.observe(廢棄:監聽標準對象的變化)
  • Process.nextTick(Node環境,通常也被認爲是微任務)

    2、宏任務(MacroTask/Task)

      基本上,我們將javascript中非微任務(MircoTask)的所有任務都歸爲宏任務,比如:

  • script中全部代碼
  • DOM操作
  • 用戶交互操作
  • 所有的網路請求
  • 定時器相關的 setTimeout、setInterval 等
  • ···

    3、javascript runtime

      javascript runtime:爲 JavaScript 提供一些對象或機制,使它能夠與外界交互,是javascript的執行環境。javascript執行時會創建一個main thread主線程call-stack 調用棧(執行棧,遵循後進先出的規則)所有的任務都會被放到調用棧/執行棧等待主線程執行。其運行機制如下:
    在這裏插入圖片描述

  • 1)主線程自上而下依次執行所有代碼;
  • 2)同步任務直接進入到主線程被執行;
  • 3)異步任務進入到Event Table,當異步任務有結果後,將相對應的回調函數進行註冊,放入Event Queue
  • 4)主線程任務執行完空閒下來後,從Event Queue(FIFO)中讀取任務,放入主線程執行;
  • 5)放入主線程的Event Queue任務繼續從第一步開始,如此循環執行;
    上述步驟執行過程就是我們所說的事件循環(Event Loop),上圖展示了事件循環中的一個完整循環過程。

    三、瀏覽器環境的Event Loop

      不同的執行環境中,Event Loop的執行機制是不同的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 實現並提供了 ECMAScript 標準中的所有數據類型、操作符、對象和方法(注意並沒有 DOM)。但它們的 Runtime 並不一樣:Chrome 提供了 window、DOM,而 Node.js 則是 require、process 等等。我們在瞭解瀏覽器中Event Loop的具體表現前需要先整理同步、異步、微任務、宏任務之間的關係!

    1、同步、異步 和 宏任務、微任務

      看到這裏,可能會有很多疑惑:同步異步很好理解,宏任務微任務上面也進行了分類,但是當他們四個在一起後就感覺很混亂了,冥冥之中覺得同步異步和宏任務微任務有內在聯繫,但是他們之間有聯繫嗎?又是什麼聯繫呢?網上有的文章說宏任務就是同步的,微任務就是異步的 這種說法明顯是錯的!
      其實我更願意如此描述:宏任務和微任務是相對而言的,根據代碼執時循環的先後,將代碼執行分層理解,在每一層(一次)的事件循環中,首先整體代碼塊看作一個宏任務,宏任務中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是該宏任務層的微任務;宏任務中的同步代碼進入主線程中立即執行的,宏任務中的非微任務異步執行代碼將作爲下一次循環的宏任務時進入調用棧等待執行的;此時,調用棧中等待執行的隊列分爲兩種,優先級較高先執行的本層循環微任務隊列(MicroTask Queue),和優先級低的下層循環執行的宏任務隊列(MacroTask Queue)!
    注意:每一次/層循環,都是首先從宏任務開始,微任務結束;
    在這裏插入圖片描述


    2、簡單實例分析

    上面的描敘相對拗口,結合代碼和圖片分析理解:


在這裏插入圖片描述

  答案暫時不給出,我們先進行代碼分析:這是一個簡單而典型的雙層循環事件循環執行案例,在這個循環中可以按照以下步驟進行分析:

  • 1、首先區分出該層宏任務的範圍(整個代碼);
  • 2、區分宏任務同步代碼異步代碼
    同步代碼:console.log('script start');console.log('enter promise');console.log('script end');
    異步代碼塊:setTimeoutPromise的then注意Promise中只有then、catch、finally的執行需要等到結果,Promise傳入的回調函數屬於同步執行代碼);

  • 3、在異步中找出同層的微任務(代碼中的Promise的then)和下層事件循環的宏任務(代碼中的setTimeout
  • 4、宏任務同步代碼優先進入主線程,按照自上而下順序執行完畢;
    輸出順序爲:
    //同步代碼執行輸出
    script start
    enter promise
    script end

  • 5、當主線程空閒時,執行該層的微任務
    //同層微任務隊列代碼執行輸出
    promise then 1
    promise then 2
  • 6、首層事件循環結束,進入第二層事件循環(setTimeout包含的執行代碼,只有一個同步代碼)
    //第二層宏任務隊列代碼執行輸出
    setTimeout

    綜合分析最終得出數據結果爲:

    //首層宏任務代碼執行輸出
    script start
    enter promise
    script end
    //首層微任務隊列代碼執行輸出
    promise then 1
    promise then 2
    //第二層宏任務隊列代碼執行輸出
    setTimeout

    3、複雜案例分析

      那麼,你是否已經瞭解上述執行過程了呢?如果完全理解上述實例,說明你已經大概知道瀏覽器中Event Loop的執行機制,但是,要想知道自己是不是完全明白,不妨對於下列多循環的事件循環進行分析檢驗,給出你的結果:

    console.log('1');
    setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('3');
        resolve();
    }).then(function() {
        console.log('4')
    })
    setTimeout(function() {
        console.log('5');
        new Promise(function(resolve) {
            console.log('6');
            resolve();
        }).then(function() {
            console.log('7')
        })
    })
    console.log('14');
    })
    new Promise(function(resolve) {
    console.log('8');
    resolve();
    }).then(function() {
    console.log('9')
    })
    setTimeout(function() {
    console.log('10');
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
    })
    console.log('13')

    分析:如下圖草稿所示,左上角標a爲宏任務隊列,左上角標i爲微任務隊列,同一層循環中,本層宏任務先執行,再執行微任務;本層宏任務中的非微任務異步代碼塊作爲下層循環的宏任務進入下次循環,如此循環執行;
    在這裏插入圖片描述

如果你的與下面的結果一致,恭喜你瀏覽器環境的Event Loop你已經完全掌握,那麼請開始下面的學習:

1->8->13->9->2->3->14->4->10->11->12->5->6->7

四、Node 環境下的 Event Loop

  在Node環境下,瀏覽器的EventLoop機制並不適用,切記不能混爲一談。這裏借用網上很多博客上的一句總結(其實我也是真不太懂):Node中的Event Loop是基於libuv實現的:libuvNode 的新跨平臺抽象層,libuv使用異步,事件驅動的編程方式,核心是提供i/o的事件循環和異步回調。libuvAPI包含有時間,非阻塞的網絡,異步文件操作,子進程等等。

1、Event Loop的6階段

在這裏插入圖片描述

  Node的Event loop一共分爲6個階段,每個細節具體如下:

  • timers: 執行setTimeout和setInterval中到期的callback。
  • pending callback: 上一輪循環中少數的callback會放在這一階段執行。
  • idle, prepare:僅在內部使用。
  • poll:最重要的階段,執行pending callback,在適當的情況下回阻塞在這個階段。
  • check:執行setImmediate的callback。
  • close callbacks: 執行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。
    注意:上面六個階段都不包括 process.nextTick()
    在這裏插入圖片描述

重點:如上圖所,在Node.js中,一次宏任務可以認爲是包含上述6個階段、微任務microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。

2、process.nextTick()

  在第二節中就瞭解到,process.nextTick()屬於微任務,但是這裏需要重點提及下:

  • process.nextTick()雖然它是異步API的一部分,但未在圖中顯示。因爲process.nextTick()從技術上講,它不是事件循環的一部分;
  • 當每個階段完成後,如果存在 nextTick,就會清空隊列中的所有回調函數,並且優先於其他 microtask 執行(可以理解爲微任務中優先級最高的

    3、實例分析

      老規矩,線上代碼:

    console.log('1');
    setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
    })
    process.nextTick(function() {
    console.log('6');
    })
    new Promise(function(resolve) {
    console.log('7');
    resolve();
    }).then(function() {
    console.log('8')
    })
    setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
    })
    console.log('13')

    將代碼的執行分區進行解釋
    在這裏插入圖片描述
    分析:如下圖草稿所示,左上角標a爲宏任務隊列,左上角標i爲微任務隊列左上角標t爲timers階段隊列左上角標p爲nextTick隊列同一層循環中,本層宏任務先執行,再執行微任務;本層宏任務中的非微任務異步代碼塊作爲下層循環的宏任務進入下次循環,如此循環執行:在這裏插入圖片描述

  • 1、整體代碼可以看做宏任務,同步代碼直接進入主線程執行,輸出1,7,13,接着執行同層微任務且nextTick優先執行輸出6,8
  • 2、二層中宏任務中只存在setTimeout,兩個setTimeout代碼塊依次進入6階段中的timer階段t1、t2進入隊列;代碼等價於:
    setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
    })
    setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
    })
  • 3、setTimeout中的同步代碼立即執行輸出2,4,9,11nextTickPormise.then進入微任務執行輸出3,10,5,12
  • 4、二層中不存在6階段中的其他階段,循環完畢,最終輸出結果爲:1->7->13->6->8->2->4->9->11->3->10->5->12

    4、當堂小考

    console.log('1');
    setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
        setTimeout(function() {
          console.log('6');
          process.nextTick(function() {
              console.log('7');
          })
          new Promise(function(resolve) {
              console.log('8');
              resolve();
          }).then(function() {
              console.log('9')
          })
      })
    })
    })
    process.nextTick(function() {
    console.log('10');
    })
    new Promise(function(resolve) {
    console.log('11');
    resolve();
    }).then(function() {
    console.log('12')
    setTimeout(function() {
      console.log('13');
      process.nextTick(function() {
          console.log('14');
      })
      new Promise(function(resolve) {
          console.log('15');
          resolve();
      }).then(function() {
          console.log('16')
      })
    })
    })
    setTimeout(function() {
    console.log('17');
    process.nextTick(function() {
        console.log('18');
    })
    new Promise(function(resolve) {
        console.log('19');
        resolve();
    }).then(function() {
        console.log('20')
    })
    })
    console.log('21')

    五、總結

      瀏覽器Node環境下,microtask 任務隊列的執行時機不同:Node 端,microtask 在事件循環的各個階段之間執行;瀏覽器端,microtask 在事件循環的 macrotask 執行完之後執行;

參考借鑑

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