我是這樣理解EventLoop的

我是這樣理解EventLoop的

在這裏插入圖片描述

1、前言

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

2、Event Loop知識鋪墊

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

一、微任務(MircoTask)

  在 HTML 標準中,並無明確規定 Microtask,可是實際開發中包含如下四種:node

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

二、宏任務(MacroTask/Task)

  基本上,咱們將javascript中非微任務(MircoTask)的全部任務都歸爲宏任務,好比:編程

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

三、javascript runtime

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

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

3、瀏覽器環境的Event Loop

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

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

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

二、簡單實例分析

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

在這裏插入圖片描述

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

  • 一、首先區分出該層宏任務的範圍(整個代碼);
  • 二、區分宏任務同步代碼異步代碼
    同步代碼:console.log('script start');console.log('enter promise');console.log('script end');
    異步代碼塊:setTimeoutPromise的then注意Promise中只有then、catch、finally的執行須要等到結果,Promise傳入的回調函數屬於同步執行代碼);
  • 三、在異步中找出同層的微任務(代碼中的Promise的then)和下層事件循環的宏任務(代碼中的setTimeout
  • 四、宏任務同步代碼優先進入主線程,按照自上而下順序執行完畢;
    輸出順序爲:
//同步代碼執行輸出
script start
enter promise
script end
  • 五、當主線程空閒時,執行該層的微任務
//同層微任務隊列代碼執行輸出
promise then 1
promise then 2
  • 六、首層事件循環結束,進入第二層事件循環(setTimeout包含的執行代碼,只有一個同步代碼)
//第二層宏任務隊列代碼執行輸出
setTimeout

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

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

三、複雜案例分析

  那麼,你是否已經瞭解上述執行過程了呢?若是徹底理解上述實例,說明你已經大概知道瀏覽器中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

4、Node 環境下的 Event Loop

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

一、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隊列的任務。

二、process.nextTick()

  在第二節中就瞭解到,process.nextTick()屬於微任務,可是這裏須要重點說起下:

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

三、實例分析

  老規矩,線上代碼:

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,7,13,接着執行同層微任務且nextTick優先執行輸出6,8
  • 二、二層中宏任務中只存在setTimeout,兩個setTimeout代碼塊依次進入6階段中的timer階段t一、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')
    })
})
  • 三、setTimeout中的同步代碼當即執行輸出2,4,9,11nextTickPormise.then進入微任務執行輸出3,10,5,12
  • 四、二層中不存在6階段中的其餘階段,循環完畢,最終輸出結果爲:1->7->13->6->8->2->4->9->11->3->10->5->12

四、當堂小考

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')

5、總結

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

參考借鑑

相關文章
相關標籤/搜索