理解瀏覽器和node.js中的Event loop事件迴圈

ReneeTsang發表於2018-03-26

大家都知道,javascript是一門單執行緒語言,因此為了實現主執行緒的不阻塞,Event Loop這樣的方案應運而生。

瀏覽器和node中Event loop並不一樣,瀏覽器的Event loop是在HTML5中定義的規範,而node中則由libuv庫實現。

瀏覽器中的Event loop

  • 所有同步任務都在主執行緒上執行,形成一個執行棧

  • 主執行緒之外,還存在一個任務佇列。

    • 任務佇列分為macro-task(巨集任務)和micro-task(微任務)。

    • macro-task(巨集任務): setTimeout, setInterval, setImmediate, I/O等

    • micro-task(微任務): process.nextTick, 原生Promise(有些實現的promise將then方法放到了巨集任務中),Object.observe(已廢棄), MutationObserver等

整個最基本的Event Loop如圖所示:

理解瀏覽器和node.js中的Event loop事件迴圈理解瀏覽器和node.js中的Event loop事件迴圈


具體過程:

  1. 瀏覽器中,先執行當前棧,執行完主執行執行緒中的任務。

  2. 取出Microtask微任務佇列中任務執行直到清空。

  3. 取出Macrotask巨集任務中 一個 任務執行。

  4. 檢查Microtask微任務中有沒有任務,如果有任務執行直到清空。

  5. 重複3和4。

整個的這種執行機制又稱為Event Loop(事件迴圈)

例子

瞭解瀏覽器的Event loop後,檢視下面例子,猜測瀏覽器是怎麼輸出的

console.log(1);
console.log(2);
setTimeout(function(){
  console.log('setTimeout1');
  Promise.resolve().then(function(){
    console.log('Promise')
  })
})
setTimeout(function(){
  console.log('setTimeout2');
})
​
//瀏覽器輸出:1 2 setTimeout1 Promise setTimeout2複製程式碼

node中的Event loop

  • 在libuv內部有這樣一個事件環機制。在node啟動時會初始化事件環。

  • node中的event loop分為6個階段,不同於瀏覽器的是,這裡每一個階段都對應一個事件佇列,node會在當前階段中的全部任務執行完,清空NextTick Queue,清空Microtask Queue,再執行下一階段。

  • 在node.js裡,process 物件代表node.js應用程式,可以獲取應用程式的使用者,執行環境等各種資訊。process.nextTick()方法將 callback 新增到next tick 佇列,並且nextTick優先順序比promise等microtask高。

  1. timers:執行setTimeout() 和 setInterval()中到期的callback。

  2. I/O callbacks:上一輪迴圈中有少數的I/Ocallback會被延遲到這一輪的這一階段執行

  3. idle, prepare:佇列的移動,僅內部使用

  4. poll:最為重要的階段,執行I/O callback,在適當的條件下會阻塞在這個階段

  5. check:執行setImmediate的callback

  6. close callbacks:執行close事件的callback,例如socket.on("close",func)

理解瀏覽器和node.js中的Event loop事件迴圈理解瀏覽器和node.js中的Event loop事件迴圈

例子

檢視下面例子加深對event loop的理解

在node執行下面程式碼,發現每次執行先後順序不一樣,因為node需要啟動時間,執行過程中setTimeout可能到時間了也可能沒到時間,所以這個先後順序取決於node的執行時間。

setTimeout(function(){
  console.log('timeout')  
})
setImmediate(function(){
  console.log('immediate')
})複製程式碼

i/o操作階段完成後,會走check階段,所以setImmediate會優先走

let fs=require('fs');
fs.readFile('./1.log',function(){
  console.log('fs');
  setTimeout(function(){
    console.log('timeout')
  })
  setImmediate(funciton(){
     console.log('setTimmediate')          
  })
})複製程式碼

nextTick應用場景

function Fn(){
  this.arrs;
  process.nextTick(()=>{ //根據nextTick的特性,可以先賦值,再在下一個佇列中使用
    this.arrs();
  })
}
Fn.prototype.then=function(){
  this.arrs=function(){console.log(1)}
}
let fn=new Fn();
fn.then();
​
//注意:nextTick千萬不要寫遞迴,不然會造成死迴圈。可以放一些比setTimeout優先執行的任務複製程式碼

總結

  • 同一個上下文下,MicroTask微任務會比MacroTask巨集任務先執行。

  • 瀏覽器是先取出一個MacroTask巨集任務執行,再執行MicroTask微任務中的所有任務。Node是按照六個階段執行,每個階段切換時,再執行MicroTask微任務佇列

  • 同個MicroTask佇列下process.tick()會優於Promise

  • setImmdieate()和setTimeout(),如果他們在非同步i/o callback之外呼叫(在i/o內呼叫因為下一階段為check階段),其執行先後順序是不確定的,需要看loop的執行前的耗時情況。


相關文章