不要混淆nodejs和瀏覽器中的event loop

1. 什麼是 Event Loop?

"Event Loop是一個程序結構,用於等待和發送消息和事件。 (a programming construct that waits for and dispatches events or messages in a program.)"
複製代碼

舉一個你們都熟知的栗子, 這樣更能客觀的理解。node

你們都知道深夜食堂吧。廚師就一我的(服務端 Server)。最多再來一個服務生( 調度員 Event Loop )。晚上吃飯的客人(客戶端 Client)不少。數據庫

1. 客人向服務生點完菜,就幹本身事情,不用一直等着服務生, 服務生把一我的點的菜單送到廚師,
   又去服務新的客人...
2. 廚師(服務端)只負責作客人們點的菜。
3. 服務生(調度員)不停的看廚師,一旦廚師作好菜了,按照標號送到相應的 客人(客戶端)座位上
複製代碼

假設咱們把 廚師 和 服務生 都比做 服務端的線程的話, 服務生線程 爲 主線程, 廚師線程 爲 消息線程。 客人每點一個菜。服務生就向廚師發出一個消息。並保留該消息的「標識」( 回調函數)用來接收廚師炒好的菜,並把菜送到相應的客人手中。

2. 同步模式

無論店裏的客人多少,也無論每一份菜須要多久的時間作好。就只有廚師這一我的忙活。廚師一次只能服務一個客人。那這樣的服務模式效率就比較低了。中途等待的時間比較長。 筆者認爲 同步模式 就是沒有 「服務生線程」, 廚師線程升級爲 主線程api

1. 第一個客人點了一份 "讀取文件" ,  炒好一份 "讀取文件"  須要花費 1 分鐘
2. 必須等第一個客人的菜炒好後,第二個客人才能點,而且點了一份 "讀取數據庫",
   炒好一份 "讀取數據庫" 須要花費 2 分鐘
3. 第三個客人點了一份 ...
複製代碼

從圖中能夠看出紅色部份都是等待時間(或者是阻塞時間), 至關浪費資源。promise

假設咱們如今只知道一種代碼的執行方式 "同步執行", 也就是代碼從上到下 按順序執行。若是遇到 setTimeout , 也先這樣理解。(實際上setTimeout 自己是當即執行的,只是回調函數異步執行)瀏覽器

console.log(1);                         //執行順序1
setTimeout(function(){}, 1000);         //執行順序2
console.log(2);                         //執行順序3
複製代碼

3. 異步模式

圖表更能直觀的反應這個概念: bash

主線程 不停的接收請求 request 和 響應請求 response, 真正處理任務的被 消息線程 event loop 安排其餘相應的程序去執行,並接收相應的相應程序返回的消息。而後 reponse 給客戶端。多線程

1. 主線程乾的事情很是簡單,即 接收請求,響應請求, 所以能夠可以處理更多的請求。而不用等待。
2. 消息線程維護請求,並把真正要作的事情交給對應的程序,並接收對應程序的回調消息,返回給 主線程
複製代碼

4. 幾種調用模式的組合

  • 同步阻塞

你跟你的女神表白,你女神當即回覆你,而你也一直再等女神的回覆異步

  • 同步不阻塞

你跟你的女神表白, 你表白後,沒有等女神來得及回覆,你去忙你本身的事情了。你的女神當即回覆了你socket

  • 異步阻塞

你跟你的女神表白, 你女神沒有當即回覆你,說要考慮考慮,過幾天答覆你,而你也一直再等女神的回覆函數

  • 異步不阻塞

你跟你的女神表白,你表白後, 沒有等女神的回覆。你去忙你本身的事情了,女神也說她要考慮考慮,過幾天再回復你

阻塞非阻塞 是指調用者(表白的那我的) 同步異步 是指被調用者 (被表白的那我的)

同步異步取決於被調用者,阻塞非阻塞取決於調用者

5. 幾個須要知曉的概念

  • 宏任務 setTimeout , setInterval, setImmediate, I / O 操做

  • 微任務 process.nextTick , 原生Promise (有些實現的Promise將then方法放到了宏任務中), Mutation Observer

console.log(1);
Promise.resolve('123').then(()=>{console.log('then')})
process.nextTick(function () {
  console.log('nextTick')
})
console.log(2);
複製代碼

process.nextTick 優先於 promise.then 方法執行

6. 瀏覽器中的Event Loop

  • 瀏覽器中js是單線程執行的。筆者稱其爲主線程, 主線程在運行過程當中會產生 堆(heap)和 棧(stack), 全部同步任務都是在 棧中執行。
function one() {
  let a = 1;
  two();
  function two() {
    console.log(a);
    let b = 2;
    function three() {
      //debugger;
      console.log(b);
    }
    three();
  }
}
one();
複製代碼

毫無疑問的是,上面這段代碼執行的結果爲:

1
2
複製代碼

在棧中都是以同步任務的方式存在:

再來看下面這段代碼:

console.log(1);
setTimeout(function(){
  console.log(2);
})
console.log(3);
複製代碼

執行結果爲:

1
3
2
複製代碼

那究竟是怎樣執行的呢?

順便提一句:文章最開始就說 setTimeout函數自己的執行時機和其回調函數執行的時機是不同的。

//宏任務
setTimeout(function(){
  console.log(2);
})

//微任務
let p = new Promise((resolve, reject) => {
  resolve(3);
});
p.then((data) => {
  console.log(data);
}, (err)=>{

})

console.log(4);
複製代碼

執行結果爲:

1
4
3
2
複製代碼

從這個能夠看到。微任務消息隊列的執行的優先於宏任務的消息隊列.

console.log(1);

//宏任務
setTimeout(function(){
  console.log(2);
})

//微任務
let p = new Promise((resolve, reject) => {
  resolve(4);
});
p.then((data) => {
  console.log(data);
}, (err)=>{

})

setTimeout(function(){
  console.log(3);
})


console.log(5);

複製代碼

執行結果爲:

1
5
4
2
3
複製代碼

每一次事件循環機制過程當中,會將當前宏任務 或者 微任務消息隊列中的任務都執行完成。而後再以前其餘隊列。

  • 對於不能進入主線程執行的代碼,筆者稱其爲異步任務, 這部分任務會進去消息隊列(callback queue), 經過 事件循環機制 (event loop) 不停調用,進入 棧中進行執行。前提是棧中當前的全部任務(同步任務)都已經執行完成。

  • 從圖中,還能夠得出這樣的結論: 異步任務是經過 WebAPIs 的方式存入 消息隊列。
  • 上述過程老是在循環執行。

7. Node中的Event Loop

咱們先來看看node是怎樣運行的:

  • js源碼首先交給node 中的v8引擎進行編譯
  • 編譯好的js代碼經過node api 交給 libuv庫 處理
  • libuv庫經過阻塞I/O和異步的方式,爲每個js任務(文件讀取等等)建立一個單獨的線程,造成多線程
  • 經過Event Loop的方式異步的返回每個任務執行的結果,而後返回給V8引擎,並反饋給用戶

Event Loop 在整個Node 運行機制中佔據着舉足輕重的地位。是其核心。

(Event Loop 不一樣階段)

每一個階段都有一個執行回調的FIFO隊列。 雖然每一個階段都有其特定的方式,但一般狀況下,當事件循環進入給定階段時,它將執行特定於該階段的任何操做, 而後在該階段的隊列中執行回調,直到隊列耗盡或回調的最大數量 已執行。 當隊列耗盡或達到回調限制時,事件循環將移至下一個階段,依此類推。

timers:此階段執行由setTimeout()和setInterval()調度的回調。
pending callbacks:執行I / O回調,推遲到下一個循環迭代。
idle,prepare:只在內部使用。
poll:檢索新的I / O事件; 執行I / O相關的回調函數;  適當時節點將在此處阻塞。
check:setImmediate()回調在這裏被調用。
close backbacks:一些關閉回調,例如 socket.on('close',...)。
複製代碼

timers階段

須要注意的是:

const fs = require('fs');

function someAsyncOperation(callback) {
  //假設須要95ms須要執行完成
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

//定義100ms後執行
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);


// 執行someAsyncOperation須要消耗95ms執行
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});
複製代碼

分析上述代碼:

  • someAsyncOperation方法時同步代碼,先在棧中執行
  • someAsyncOperation 中包含異步I/O, 須要花費95ms執行,加上 while的10ms, 所以須要105ms
  • setTimeout 雖然定義的是在100ms後執行, 但因爲 第一次輪詢是到了 poll 階段, 因此 setTimeout 須要等到第二輪事件輪詢是執行。所以是在 105ms後執行

pending callbacks階段

此階段爲某些系統操做(如TCP錯誤類型)執行回調。例如,
若是嘗試鏈接時TCP套接字收到ECONNREFUSED,則某些* nix系統要等待報告錯誤。這將排隊等候在待處理的回調階段執行。
複製代碼

poll階段

1.計算應該阻塞和輪詢I / O的時間
2.處理輪詢隊列中的事件。
複製代碼

當事件循環進入poll階段而且沒有計時器時,會發生如下兩件事之一:

1. 若是輪詢隊列不爲空,則事件循環將遍歷其回調隊列,同步執行它們,直到隊列耗盡或達到系統相關硬限制。
2. 若是輪詢隊列爲空,則會發生如下兩件事之一:
   2.1 若是腳本已經過setImmediate()進行調度,則事件循環將結束輪詢階段並繼續執行(check階段)檢查階段以執行這些預約腳本。
   2.2 若是腳本沒有經過setImmediate()進行調度,則事件循環將等待將回調添加到隊列中,而後當即執行它們。
複製代碼

一旦輪詢隊列爲空,事件循環將檢查已達到時間閾值的定時器。若是一個或多個定時器準備就緒,則事件循環將回退到定時器階段以執行這些定時器的回調。

// poll的下一個階段時check
// 有check階段就會走到check中
let fs = require('fs');
fs.readFile('./1.txt',function () {  //輪詢隊列已經執行完成,爲空,即2.1中描述的
  setTimeout(() => {
    console.log('setTimeout')
  }, 0);
  setImmediate(() => {
    console.log('setImmediate')
  });
});
複製代碼

上面這段代碼執行的過程階段爲:

check階段

setImmediate()其實是一個特殊的定時器,它在事件循環的一個單獨的階段中運行。它使用libuv API來調度回調,以在輪詢(poll)階段完成後執行。

close callback階段

若是套接字socks或句柄忽然關閉(例如socket.destroy()),則在此階段將發出'close'事件。 不然它將經過process.nextTick()觸發事件。
複製代碼

8. setImmediate 與 setTimeout

setImmediate()用於在當前輪詢階段完成後執行腳本。
setTimeout()計劃腳本在通過最小閾值(以毫秒爲單位)後運行。
複製代碼

定時器執行的順序取決於它們被調用的上下文。 若是二者都是在主模塊內調用的,那麼時序將受到進程性能的限制(可能會受到計算機上運行的其餘應用程序的影響)。

簡言之: setTimediate 和 setTimeout 的執行順序不肯定。

// setTimeout和setImmediate順序是不固定,看node準備時間
 setTimeout(function () {
   console.log('setTimeout')
 },0);

 setImmediate(function () {
   console.log('setImmediate')
 });

複製代碼

輸出的結果多是這樣

setTimeout
setImmediate
複製代碼

也有多是這樣

setImmediate
setTimeout
複製代碼

But, 若是在I / O週期內移動這兩個調用,則當即回調老是首先執行, 能夠爬樓參考 poll階段的介紹。

使用setImmediate()的主要優勢是,若是在I / O週期內進行調度,將始終在任何計時器以前執行setImmediate(),而無論有多少個計時器。

9. process.nextTick

爲何要用process.nextTick

容許用戶處理錯誤,清理任何不須要的資源,或者可能在事件循環繼續以前再次嘗試請求。 有時須要在調用堆棧解除以後但事件循環繼續以前容許回調運行。

process.nextTick()沒有顯示在圖中,即便它是異步API的一部分。 這是由於process.nextTick()在技術上並非事件循環的一部分。 相反,nextTickQueue將在當前操做完成後處理,而無論事件循環的當前階段如何。

回顧一下事件循環機制,只要你在給定的階段調用process.nextTick(),全部傳遞給process.nextTick()的回調都將在事件循環繼續以前被解析。

// nextTick是隊列切換時執行的,timer->check隊列 timer1->timer2不叫且
setImmediate(() => {
  console.log('setImmediate1')
  setTimeout(() => {
    console.log('setTimeout1')
  }, 0);
})
setTimeout(()=>{
  process.nextTick(()=>console.log('nextTick'))
  console.log('setTimeout2')
  setImmediate(()=>{
    console.log('setImmediate2')
  })
},0);
複製代碼

在討論事件循環(Event Loop)的時候,要時刻知道 宏任務,微任務,process.nextTick等概念。 上面代碼執行的結果可能爲:

setTimeout2
nextTick
setImmediate1
setImmediate2
setTimeout1
複製代碼

或者

setImmediate1
setTimeout2
setTimeout1
nextTick
setImmediate2
複製代碼

爲何呢? 這個就留給各位看官的一個思考題吧。歡迎留言討論~

相關文章
相關標籤/搜索