成爲自信的node.js 開發者(二)

這一章,咱們來學習一下event_loop, 本文內容旨在釐清瀏覽器(browsing context)和Node環境中不一樣的 Event Loop。javascript

首先清楚一點:瀏覽器環境和 node環境的event-loop 徹底不同。java

瀏覽器環境

爲了協調事件、用戶交互、腳本、UI渲染、網絡請求等行爲,用戶引擎必須使用Event Loopevent loop包含兩類:基於browsing contexts,基於worker。node

本文討論的瀏覽器中的EL基於browsing contextspromise

上面圖中,關鍵性的兩點:瀏覽器

同步任務直接進入主執行棧(call stack)中執行bash

等待主執行棧中任務執行完畢,由EL將異步任務推入主執行棧中執行網絡

task——宏任務

task在網上也被成爲macrotask (宏任務)dom

宏任務分類:

script代碼異步

setTimeout/setIntervalsocket

setImmediate (未實現)

I/O

UI交互

宏任務特徵

一個event loop 中,有一個或多個 task隊列。

不一樣的task會放入不一樣的task隊列中:好比,瀏覽器會爲鼠標鍵盤事件分配一個task隊列,爲其餘的事件分配另外的隊列。

先進隊列的先被執行

microtask——微任務

微任務

微任務的分類

一般下面幾種任務被認爲是microtask

promise(promisethencatch纔是microtask,自己其內部的代碼並非)

MutationObserver

process.nextTick(nodejs環境中)

微任務特性

一個EL中只有一個microtask隊列。

event-loop的循環過程

一個EL只要存在,就會不斷執行下邊的步驟:

先執行同步代碼,全部微任務,一個宏任務,全部微任務(,更新渲染),一個宏任務,全部微任務(,更新渲染)...... 執行完microtask隊列裏的任務,有可能會渲染更新。在一幀之內的屢次dom變更瀏覽器不會當即響應,而是會積攢變更以最高60HZ的頻率更新視圖

例子

setTimeout(() => console.log('setTimeout1'), 0);
setTimeout(() => {
    console.log('setTimeout2');
    Promise.resolve().then(() => {
        console.log('promise3');
        Promise.resolve().then(() => {
            console.log('promise4');
        })
        console.log(5)
    })
    setTimeout(() => console.log('setTimeout4'), 0);
}, 0);
setTimeout(() => console.log('setTimeout3'), 0);
Promise.resolve().then(() => {
    console.log('promise1');
})
複製代碼

打印出來的結果是 :

promise1
setTimeout1
setTimeout2
'promise3'
5
promise4
setTimeout3
setTimeout4
複製代碼

另一個例子:

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
}
async1()

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

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
    .then(function () {
        console.log('promise1')
        setTimeout(() => {
            console.log('sssss')
        }, 0)
    })
    .then(function () {
        console.log('promise2')
    })

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

在瀏覽器內輸出結果以下, node內輸出結果不一樣

'script start'
'async2 end'
'Promise'
'script end'
'async1 end'
'promise1'
'promise2'
'setTimeout'
'sssss'
複製代碼
  1. await 只是 fn().then() 這些寫法的語法糖,至關於 await 那一行代碼下面的代碼都被當成一個微任務,推入到了microtask queue

  2. 順序:執行完同步任務,執行微任務隊列中的所有的微任務,執行一個宏任務,執行所有的微任務

node 環境中

Node中的event-looplibuv庫 實現,js是單線程的,會把回調和任務交給libuv

event loop 首先會在內部維持多個事件隊列,好比 時間隊列、網絡隊列等等,而libuv會執行一個至關於 while true的無限循環,不斷的檢查各個事件隊列上面是否有須要處理的pending狀態事件,若是有則按順序去觸發隊列裏面保存的事件,同時因爲libuv的事件循環每次只會執行一個回調,從而避免了 競爭的發生

我的理解,它與瀏覽器中的輪詢機制(一個task,全部microtasks;一個task,全部microtasks…)最大的不一樣是,node輪詢有phase(階段)的概念,不一樣的任務在不一樣階段執行,進入下一階段以前執行全部的process.nextTick() 和 全部的microtasks。

階段

timers階段

在這個階段檢查是否有超時的timer(setTimeout/setInterval),有的話就執行他們的回調

但timer設定的閾值不是執行回調的確切時間(只是最短的間隔時間),node內核調度機制和其餘的回調函數會推遲它的執行

由poll階段來控制何時執行timers callbacks
複製代碼

I/O callback 階段

處理異步事件的回調,好比網絡I/O,好比文件讀取I/O,當這些事件報錯的時候,會在 `I/O` callback階段執行
複製代碼

poll 階段

這裏是最重要的階段,poll階段主要的兩個功能:

   處理poll queue的callbacks
    
   回到timers phase執行timers callbacks(當到達timers指定的時間時)
    

進入poll階段,timer的設定有下面兩種狀況:

1.  event loop進入了poll階段, **未設定timer**
    
       poll queue不爲空:event loop將同步的執行queue裏的callback,直到清空或執行的callback到達系統上限
        
       poll queue爲空
        
           若是有設定` callback`, event loop將結束poll階段進入check階段,並執行check queue (check queue是 setImmediate設定的)
            
           若是代碼沒有設定setImmediate() callback,event loop將阻塞在該階段等待callbacks加入poll queue
            
2.  event loop進入了 poll階段, **設定了timer**
    
       若是poll進入空閒狀態,event loop將檢查timers,若是有1個或多個timers時間時間已經到達,event loop將回到 timers 階段執行timers queue
        

這裏的邏輯比較複雜,流程能夠藉助下面的圖進行理解:

![](https://ws1.sinaimg.cn/large/006tKfTcgy1g0anodoa11j311i0h0t8w.jpg)
複製代碼

check 階段

一旦poll隊列閒置下來或者是代碼被`setImmediate`調度,EL會立刻進入check phase
複製代碼

close callbacks

關閉I/O的動做,好比文件描述符的關閉,鏈接斷開等

若是socket忽然中斷,close事件會在這個階段被觸發
複製代碼

同步的任務執行完,先執行徹底部的process.nextTick() 和 所有的微任務隊列,而後執行每個階段,每一個階段執行完畢後,

注意點

setTimeout 和 setImmediate

  1. 調用階段不同

  2. 不一樣的io中,執行順序不保證

兩者很是類似,區別主要在於調用時機不一樣。

setImmediate 設計在poll階段完成時執行,即check段;

setTimeout 設計在poll階段爲空閒時,且設定時間到達後執行,但它在timer階段執行

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

對於以上代碼來講,setTimeout 可能執行在前,也可能執行在後。 首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的。

若是在準備時候花費了大於 1ms 的時間,那麼在 timer 階段就會直接執行 setTimeout 回調。 若是準備時間花費小於 1ms,那麼就是 setImmediate 回調先執行了。

也就是說,進入事件循環也是須要成本的。有可能進入event loop 時,setTimeout(fn, 1) 還在等待timer中,並無被推入到 time 事件隊列,而setImmediate 方法已經被推入到了 check事件隊列 中了。那麼event_loop 按照timei/opollcheckclose 順序執行,先執行immediate 任務。

也有可能,進入event loop 時,setTimeout(fn, 1) 已經結束了等待,被推到了time 階段的隊列中,以下圖所示,則先執行了timeout 方法。

因此,setTimeout setImmediate 哪一個先執行,這主要取決於,進入event loop 花了多長時間。

但當兩者在異步i/o callback內部調用時,老是先執行setImmediate,再執行setTimeout

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
複製代碼

在上述代碼中,setImmediate 永遠先執行。由於兩個代碼寫在 IO 回調中,IO 回調是在 poll 階段執行,當回調執行完畢後隊列爲空,發現存在 setImmediate 回調,因此就直接跳轉到 check 階段去執行回調了。

process.nextTick() 和 setImmediate()

官方推薦使用 setImmediate(),由於更容易推理,也兼容更多的環境,例如瀏覽器環境

process.nextTick() 在當前循環階段結束以前觸發

setImmediate() 在下一個事件循環中的check階段觸發

經過process.nextTick()觸發的回調也會在進入下一階段前被執行結束,這會容許用戶遞歸調用 process.nextTick() 形成I/O被榨乾,使EL不能進入poll階段

所以node做者推薦咱們儘可能使用setImmediate,由於它只在check階段執行,不至於致使其餘異步回調沒法被執行到

例子

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
複製代碼

注意:主棧執行完了以後,會先清空 process.nextick() 隊列和microtask隊列中的任務,而後按照每個階段來執行先處理異步事件的回調,好比網絡I/O,好比文件讀取I/O。當這些I/O動做都結束的時候,在這個階段會觸發它們的

另一個例子

const {readFile} = require('fs')

setTimeout(() => {
    console.log('1')
}, 0)

setTimeout(() => {
    console.log('2')
}, 100)

setTimeout(() => {
    console.log('3')
}, 200)

readFile('./test.js', () => {
    console.log('4')
})

readFile(__filename, () => {
    console.log('5')
})

setImmediate(() => {
    console.log('當即回調')
})

process.nextTick(() => {
    console.log('process.nexttick的回調')
})

Promise.resolve().then(() => {

    process.nextTick(() => {
        console.log('nexttick 第二次回調')
    })
    console.log('6')
}).then(() => {
    console.log('7')
})
複製代碼

上面代碼的結果是:

process.nexttick的回調
6
7
nexttick 第二次回調
1
當即回調
4
5
2
3
複製代碼

上面代碼須要注意點:

  1. 下面兩個回調任務,要等100ms200ms 才能被推入到timers 階段的任務隊列

  2. 兩個讀取文件的回調,須要等待讀取完成後,才能被推入到 poll 階段的任務隊列。(不是被推入到 io 階段的任務隊列,只有讀取失敗等異常的回調,纔會被推入到 io 階段的任務隊列)

  3. 在微任務裏面,新添加的process.nextTick() 也會在新階段的開始以前被執行。簡單理解爲,在每個階段的任務隊列開始以前,都須要所有清空process.nextTickmicrotask 任務隊列

一個誤區

本身在驗證上面的想法的時候,實驗過不少代碼,從未失手過,可是當實驗到下面的代碼時:

Promise.resolve().then(() => {
    console.log(1)
    Promise.resolve().then(() => {
        console.log(2)
    })
}).then(() => {
    console.log(3)
})
複製代碼

按照上面咱們講的,這裏應該是輸出132, 可是反覆驗證,在 node 實際輸出的是 123,連續好幾天都不得其解,後來看到一個問答,才恍然大悟: stackoverflow.com/questions/3…

首先,上面的代碼,在.then() 的回調函數中去執行promise.resolve(), 其實是, 在目前的promise 鏈中新建了一個獨立的 promise鏈 。 你沒有任何辦法保證這兩個哪一個先執行完,這其實是node引擎 的一個bug,就像一口氣發出兩個請求,並不知道哪一個請求先返回。

每次咱們都能獲得相同的結果是由於,咱們Promise.resolve()裏面剛好沒有異步的操做,這並非event-loop 專門設計成這樣的。

因此,沒必要花太多的時間,在上面的代碼中,實際寫代碼中,也不會出現這種狀況。

相關文章
相關標籤/搜索