瀏覽器和Node.js中的Event Loop

前言

衆所周知,javascript是一門單線程語言,而當咱們使用ajax和服務端進行通訊的時候是須要必定時間的,這樣當前線程就會被阻塞,使瀏覽器失去相應。所以,當js執行執行一些長時間的任務時,咱們但願有一種異步的方式處理這種任務。事件循環(event loop)就是如何處理異步執行順序的一種機制。javascript

$.get(url, function (data) {
    //do something
});
複製代碼

瀏覽器中的事件循環

接下來會一一介紹,事件循環中的執行棧事件隊列宏任務微任務等概念java

什麼是執行棧

執行棧就是js代碼運行的地方,上圖call stack所示。當下面程序運行時,會推送的調用棧中被執行。node

console.log('Hi');
setTimeout(function cb1() { 
    console.log('cb1');
}, 500);
console.log('Bye');
複製代碼

什麼是事件隊列

當瀏覽器中的事件監聽函數被觸發(DOM)、網絡請求的相應(ajax)、定時器被觸發(setTimeout)相對應的回調函數就會被推送到事件隊列中,等待執行;如上圖中的Callback Queue。ajax

什麼是事件循環

事件循環是一個這樣的過程:當執行棧中的任務結束以後,會將事件隊列中的第一個任務推入到執行棧中執行,當任務處理完畢,又會取事件隊列中的第一個任務,如此往復,便構成了事件循環。promise

對應到下面代碼中。瀏覽器

console.log('Hi');
setTimeout(function cb1() { 
    console.log('cb1');
}, 500);
console.log('Bye');
複製代碼
  • 程序推送到執行棧中被執行
  • 執行console語句、輸出Hi
  • 執行setTimeou語句
  • 執行console語句、輸出Bye
  • 500ms的時候,setTimeout的回調函數被推送到事件隊列中
  • 此時事件隊列中只有setTimeout的回調函數這一個任務,會被推到執行棧中執行
  • console語句執行、輸出cb1

經過上面的例子會對執行棧和事件隊列有個基本的認識。因爲JS是單線程的,同步任務會形成瀏覽器阻塞,咱們把任務分紅一個一個的異步任務,經過事件循環來執行事件隊列中的任務。這就使得當咱們掛起某一個任務的時候能夠去作一些其餘的事情,而不須要等待這個任務執行完畢。因此事件循環的運行機制大體分爲如下步驟:bash

一、檢查事件隊列是否爲空,若是爲空,則繼續檢查;如不爲空,則執行 2;網絡

二、取出事件隊列的首部,壓入執行棧;dom

三、執行任務;異步

四、檢查執行棧,若是執行棧爲空,則跳回第 1 步;如不爲空,則繼續檢查;

瀏覽器渲染時機

咱們知道DOM操做會觸發瀏覽器渲染,如增、刪節點,改變背景顏色。那麼這類操做是如何在瀏覽器當中奏效的?

至此咱們已經知道了事件循環是如何執行的,事件循環器會不停的檢查事件隊列,若是不爲空,則取出隊首壓入執行棧執行。當一個任務執行完畢以後,事件循環器又會繼續不停的檢查事件隊列,不過在這間,瀏覽器會對頁面進行渲染。這就保證了用戶在瀏覽頁面的時候不會出現頁面阻塞的狀況,這也使 JS 動畫成爲可能。

function move() {
    setTimeout(() => {
        dom.style.left = dom.offsetLeft + 10 + 'px'
        move()
    }, 15);
}
move()
複製代碼

如今用事件循環的機制說明js動畫的過程。上面代碼會在執行棧中執行,move函數被調用,setTimeout的回調函數15ms以後會被推送到事件隊列中。此時執行棧中的任務結束,瀏覽器渲染、檢查事件隊列不斷循環。當15ms以後事件隊列中有任務時,會被推送到執行棧中執行,這時dom節點向右偏移10px,move函數執行、執行棧結束,瀏覽渲染、檢查事件隊列。如此往復就造成了動畫。

宏任務和微任務(microtask)

先看一段代碼,是如何輸出的;

console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0);
Promise.resolve().then(function () {
    console.log('promise1');
}).then(function () {
    console.log('promise2');

});
console.log('script end');
複製代碼

答案是:'script start''script end''promise1''promise2''setTimeout'

setTimeout的回調函數是宏任務、Promise的回調函數是微任務。微任務和宏任務同樣遵循事件循環機制,可是他們仍是有些差異。

一、宏任務和微任務的事件隊列是相互獨立的;

二、微任務隊列的檢查時機早於宏任務。(執行棧中任務結束就會立刻清空微任務事件隊列)

根據上面的規則,解釋代碼的輸出。

  • 執行棧中的代碼執行,宏任務推入宏任務事件隊列、微任務推入微任務事件隊列,執行棧任務結束

  • 檢查微任務事件隊列,此時已經有Promise的回調函數,推入執行棧,輸出promise1。Promise還有回調函數,推入微任務事件隊列,執行棧結束。

  • 檢查微任務事件隊列,推入執行棧,輸出promise2,執行棧結束。

  • 檢查微任務事件隊列,此時被清空

  • 檢查宏任務事件隊列,推入執行棧,輸出setTimeout,執行棧結束。

    宏任務有: **setTimeout** 、**setImmediate** 、 **MessageChannel**
      微任務有: **setTimeout** 、**setImmediate** 、 **MessageChannel**
    複製代碼

Node.js中的事件循環

Node中的事件循環是和瀏覽器有很大區別的

當Node.js啓動時,會初始化event loop;每一個event loop都會包含按以下順序六個循環階段

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────
複製代碼
  • timers 階段: 這個階段執行setTimeout(callback) and setInterval(callback)預約的callback;
  • I/O callbacks 階段: 執行除了 close事件的callbacks、被timers(定時器,setTimeout、setInterval等)設定的callbacks、setImmediate()設定的callbacks以外的callbacks;
  • idle, prepare 階段: 僅node內部使用;
  • poll 階段: 獲取新的I/O事件, 適當的條件下node將阻塞在這裏;
  • check 階段: 執行setImmediate() 設定的callbacks;
  • close callbacks 階段: 好比socket.on(‘close’, callback)的callback會在這個階段執行。

每個階段都有一個裝有callbacks的fifo queue(隊列),當event loop運行到一個指定階段時, node將執行該階段的fifo queue(隊列),當隊列callback執行完或者執行callbacks數量超過該階段的上限時,event loop會轉入下一下階段。

Node.js中的宏任務和微任務

宏任務:setTimeout和setImmediate
複製代碼
  • setTimeout 設計在poll階段爲空閒時,且設定時間到達後執行;但其在timer階段執行
  • setImmediate 設計在check階段執行;

誰先輸出,誰後輸出?

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});
複製代碼

答案是不肯定的。有兩個前提咱們是須要清楚的;

  • event loop初始化是須要必定時間的
  • setTimeout有最小毫秒數的,一般是4ms。

當:event loop準備時間 > setTimeout最小毫秒數。從timers階段檢查,此時隊列中已經有setTimeout的任務,因此timeout先輸出;

當:event loop準備時間 < setTimeout最小毫秒數。從timers階段檢查,此時隊列是空的就下檢查接下來的階段,到check階段,已經有setImmediate的任務,因此immediate先輸出;

微任務:process.nextTick()和Promise.then()
複製代碼

微任務不在event loop的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行;nextTick比Promise.then()先執行

下面代碼是如何執行的。

setImmediate(() => {
  console.log('setImmediate1')
  setTimeout(() => {
    console.log('setTimeout1')
  }, 0);
})
setTimeout(()=>{
  process.nextTick(()=>console.log('nextTick'))
  console.log('setTimeout2')
  setImmediate(()=>{
    console.log('setImmediate2')
  })
},0);
複製代碼
  • 從前面的知識知道,此時setTimeout和setImmediate執行順序是不肯定的。
  • 假設setImmediate先執行,輸出setImmediate1,setTimeout的任務添加到timer階段
  • 檢查timer階段,這時已經有兩個任務。先執行以前的第一個任務,nextTick添加到微任務隊列,輸出setTimeout2,setImmediate的任務添加到check階段。
  • timer中還有一個任務,執行輸出setTimeout1
  • 切換階段,微任務執行,輸出nextTick
  • 檢查check階段,輸出setImmediate2

思考題

let fs = require('fs')

fs.readFile('./1.txt', 'utf8', function (err, data) {
    setTimeout(() => {
        console.log('setTimeout')
    }, 0);
    setImmediate(() => {
        console.log('setImmediate')
    })
})
複製代碼

這種狀況下的setTimeout和setImmediate執行的順序肯定嗎?readFile的回調函數是在poll階段執行 答案是setImmediatesetTimeout先執行

結語

瀏覽器中和Node.js中的事件循環能夠說是兩套不一樣的機制,作個總結,但願有所幫助。

相關文章
相關標籤/搜索