js事件迴圈機制 EventLoop 【瀏覽器和node】

More發表於2018-06-29

js的事件迴圈機制

經典面試題

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

setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
       console.log('promise2')
    })
}, 0)
複製程式碼

這段程式碼在瀏覽器裡列印的結果是 timer1 promise1 timer2 promise2
而在node中列印的卻是 timer1 timer2 promise1 promise2
所以我們來看一下瀏覽器和node的事件機制的區別

瀏覽器篇

提到js,就會想到單執行緒,非同步,那麼單執行緒是如何做到非同步的呢?概念先行,先要了解下單執行緒和非同步之間的關係。

1.單執行緒和非同步

  • 單執行緒是指js引擎中負責解析執行js程式碼的執行緒只有一個
  • 非同步是指在主執行緒在等待X響應的同時是會去做其它事

2.呼叫棧和任務佇列

顧名思義,呼叫棧是一個棧結構,函式呼叫會形成一個棧幀,幀中包含了當前執行函式的引數和區域性變數等上下文資訊,函式執行完後,它的執行上下文會從棧中彈出。
下圖就是呼叫棧和任務佇列的關係圖

js事件迴圈機制 EventLoop 【瀏覽器和node】

3.非同步任務:巨集任務(macrotask)& 微任務(microtask)

從規範理解,瀏覽器至少有一個事件迴圈,一個事件迴圈至少有一個任務佇列(macrotask),每個外任務都有自己的分組,瀏覽器會為不同的任務組設定優先順序。

巨集任務:script程式碼執行、setInterval、 setTimeout setImmediateMessageChannel (常見的)

微任務:Promise.then、 MutaionObserver(有相容問題)、process.nextTick(Node.js環境)

其中setImmediate和process.nextTick是nodejs的實現,在nodejs篇會詳細介紹。 下圖就是補充上的巨集任務和微任務的事件機制

js事件迴圈機制 EventLoop 【瀏覽器和node】
總結起來,一次事件迴圈的步驟包括:

  1. 首先執行同步任務
  2. 遇到非同步任務,按照巨集任務和微任務分別放置。
  3. 執行微任務,並清空本次迴圈的微任務佇列。
  4. 執行排在第一個的巨集任務。
  5. 再次檢視微任務佇列,形成迴圈。

這是補上巨集任務和微任務的迴圈圖。

js事件迴圈機制 EventLoop 【瀏覽器和node】

請看練習題

Promise.resolve().then(function() {
    console.log('promise3')
    setTimeout(() => {
        console.log('timer1')

        Promise.resolve().then(function() {
            console.log('promise1')
        })
    }, 100)

})
setTimeout(() => {
    console.log('timer2')

    Promise.resolve().then(function() {
        setTimeout(() => {
            console.log('time3')
        }, 100)
        console.log('promise2')
    })
}, 0) 

複製程式碼

我們來分析一下: 1.首先讀讀取程式碼,微任務promise3 放入微任務佇列,巨集任務setTimeouttimer2放入巨集任務佇列。
2.先執行微任務,列印微任務promise3,讀取setTimeout timer1,在100秒後將其放入巨集任務佇列,並在timer2 後面。
3.執行巨集任務,列印timer2。並執行微任務,列印promise2,並執行後面的程式碼,100秒後將time3放入巨集任務。

Promise.resolve().then(function() {
        setTimeout(() => {
            console.log('time3')
        }, 100)
        console.log('promise2')
    })

複製程式碼

4.100秒後執行列印timer1,執行微任務 列印promise1 5.執行下一個巨集任務 列印time3 答案就是promise3 timer2 promise2 timer1 promise1 time3

node篇

node是什麼

Node.js是一個基於 Chrome V8 引擎的JavaScript執行環境(runtime); Node不是一門語言是讓js執行在後端的執行時。

node可以做什麼

Node在處理高併發,I/O密集場景有明顯的效能優勢
高併發,是指在同一時間併發訪問伺服器
I/O密集指的是檔案操作、網路操作、資料庫,相對的有CPU密集,
CPU密集指的是邏輯處理運算、壓縮、解壓、加密、解密

node可以做什麼

當應用程式需要處理大量併發的輸入輸出,而在向客戶端響應之前,應用程式並不需要進行非常複雜的處理。

  • 聊天伺服器
  • 電子商務網站

node是怎麼工作的

1.我們寫的js程式碼會交給v8引擎進行處理
2.程式碼中可能會呼叫nodeApi,node會交給libuv庫處理
3.libuv通過阻塞i/o和多執行緒實現了非同步io
4.通過事件驅動的方式,將結果放到事件佇列中,最終交給我們的應用。

js事件迴圈機制 EventLoop 【瀏覽器和node】

  • 在libuv內部有這樣一個事件環機制。 在node啟動時會初始化事件環。 事件迴圈會無限次地執行,一輪又一輪。 只有非同步任務的回撥函式佇列清空了,才 會停止執行。每一輪的事件迴圈,分成六 個階段。這些階段會依次執行。
  • 為了協調非同步任務,Node 提供了四個定時器, 讓任務可以在指定的時間執行。
    -- setTimeout()
    -- setInterval()
    -- setImmediate()
    -- process.nextTick()【微任務】
    js事件迴圈機制 EventLoop 【瀏覽器和node】

回顧瀏覽器環境下,microtask的任務佇列 是每個macrotask執行完之後執行
而在Node.js中,microtask會在事件迴圈的 各個階段之間執行,也就是一個階段執行完畢, 就會去執行microtask佇列的任務。並且process.nextTick()會優先執行

js事件迴圈機制 EventLoop 【瀏覽器和node】

timers 階段

這個是定時器階段,處理setTimeout()和setInterval()的回撥函式。 進入這個階段後,主執行緒會檢查一下當前時間,是否滿足定時器的條件。 如果滿足就執行回撥函式,否則就離開這個階段。

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

setImmediate(() => { console.log('immediate') }) // 執行時間不確定。

1.setTimeout在 timers 階段執行,而setImmediate在 check 階段執行,所以是timeout/immediate

2.setTimeout的第二個引數預設為0,但是實際上,Node 做不到0毫秒,最少也需要1毫秒,根據官方文件, 第二個引數的取值範圍在1毫秒到2147483647毫秒之間。也就是說,setTimeout(f, 0)等同於setTimeout(f, 1)。 實際執行的時候,進入事件迴圈以後,有可能到了1毫秒,也可能還沒到1毫秒,取決於系統當時的狀況, 如果沒到1毫秒,那麼 timers 階段就會跳過,進入 check 階段,先執行setImmediate的回撥函式。

poll階段

這個階段是輪詢時間,用於等待還未返回的 I/O 事件, 比如伺服器的迴應、使用者移動滑鼠等等。 這個階段的時間會比較長。如果沒有其他非同步任務要處理(比如到期的定時器), 會一直停留在這個階段,等待 I/O 請求返回結果

check 階段

執行setImmediate

最一道測試題,如果能全部回答正確 說明你就掌握了事件機制了哦!

let fs = require('fs');
fs.readFile('./1.txt', function(err, data) {
   console.log('1讀取檔案')
});
setTimeout(() => {
   console.log('2')

}, 0)
console.log('3')
Promise.resolve().then(function() {
   console.log('4')
   setTimeout(() => {
       console.log('5')

       Promise.resolve().then(function() {
           console.log('6')
       })
   }, 300)

})
setTimeout(() => {
   console.log('7')

}, 300)
process.nextTick(() => console.log(8));
//3,8,4,2,1,7,5,6

複製程式碼

相關文章