node事件循環中事件執行的順序

事件循環

在瀏覽器環境下我們的js有一套自己的事件循環,同樣在node環境下也有一套類似的事件循環。

瀏覽器環境事件循環

首先,我們先來回顧一下在瀏覽器的事件循環:

總結來說:

首先會運行主線程的同步代碼,每一行同步代碼都會被壓入執行棧,每一行異步代碼會壓入異步API中(如:定時器線程、ajax線程等;),在執行棧沒有要執行的代碼時,也就是我們當前主線程沒有同步代碼瞭,任務隊列會從我們的異步任務微任務隊列中取一個微任務放到我們的任務隊列中進行執行,將它的回調函數進而再次放到執行棧中進行執行,當微任務隊列為空時,會在宏任務中取異步任務加到任務隊列,進而壓入執行棧,執行回調函數,然後繼續在該宏任務中查找同步、異步任務,一次循環,完成瞭一個事件循環(事件輪詢)

瀏覽器環境下的例子:

例子:

        console.log("1");
        setTimeout(() => {
            console.log("setTimeout");
        }, 1);
        new Promise((res, rej) => {
            console.log("Promise");
            res('PromiseRes')
        }).then(val => {
            console.log(val);
        })
        console.log("2");

分析:
首先執行棧找到第一行的同步代碼,直接扔到執行棧中執行,打印1,隨後為定時器setTimeout,為異步任務,將代碼放到異步對列中等待執行,隨後執行promise中的代碼,我們要清楚promise是同步執行,它的回調是異步執行,所有打印Promise,將res(‘PromiseRes’)放到異步對列中等待執行,這個時候又遇到瞭同步代碼,打印2,當前主線程的同步代碼全部執行完畢,並且執行棧中沒有要執行的同步代碼,這個時候webApi會從異步隊列中去微任務隊列中的第一個,加入到事件隊列執行,將返回的回調函數壓入到執行棧中執行,打印PromiseRes,隨後微任務執行完畢,已經沒有微任務,現在就需要從宏任務隊列中取宏任務定時器,加入到任務隊列中,將回調函數壓入到執行棧中執行,打印setTimeout。

node環境事件循環

在node中事件循環主要分為六個階段來實現:

外部數據輸入–》輪詢階段–》檢查階段–》關閉事件回調階段–》定時器階段–》I/O回調階段–》閑置階段–》輪詢階段》…開始循環

六個階段

圖片來自網絡

在這裡插入圖片描述

  • timers階段:用來執行timer(setTimeout,setInterval)的回調;
  • I/O callbacks階段:處理一些上一輪循環中少數未執行的I/O回調
  • idle,prepare 階段:僅node內部使用,我們用不到;
  • poll階段:獲取新的I/O時間,適當的條件下node將阻塞在這裡;
  • check階段:執行setImmediate()的回調;
  • close callbacks 階段:執行socket的close時間回調

主要階段
timer:
timers階段會執行setTimeout和setInterval回調,並且是由poll階段控制的。
同樣,在node中定時器指定的時間也不是準確時間,隻能是盡快執行。
poll:
poll這一階段中,系統會做兩件事情:
1.回到timer階段執行回調
2.執行I/O回調
並且在進入該階段時如果沒有設定瞭timer 的話,會發生以下兩件事情

如果 poll 隊列不為空,會遍歷回調隊列並同步執行,直到隊列為空或者達到系統限制
如果 poll 隊列為空時,會有兩件事發生
1、如果有 setImmediate 回調需要執行,poll 階段會停止並且進入到 check 階段執行回調
2、如果沒有 setImmediate 回調需要執行,會等待回調被加入到隊列中並立即執行回調,這裡同樣會有個超時時間設置防止一直等待下去
當然設定瞭 timer 的話且 poll 隊列為空,則會判斷是否有 timer 超時,如果有的話會回到 timer 階段執行回調。

check階段
setImmediate()的回調會被加入 check 隊列中,從 event loop 的階段圖可以知道,check 階段的執行順序在 poll 階段之後,在進入check階段執勤poll會檢查有的話到check階段,沒有的換直接到timer階段。

(1) setTimeout 和 setImmediate

二者非常相似,區別主要在於調用時機不同。

setImmediate 設計在 poll 階段完成時執行,即 check 階段,隻有在check階段才會執行;
setTimeout 設計在 poll 階段為空閑時,且設定時間到達後執行,但它在 timer 階段執行,表示當前線程沒有其他可執行的同步任務,才會在timer階段執行定時器。

這兩個執行的時機可前可後:
例子1:

// //異步任務中的宏任務
setTimeout(() => {
    console.log('===setTimeout===');
},0);
setImmediate(() => {
    console.log('===setImmediate===')
})

在這裡插入圖片描述

多次重復執行的結果會不同,有一種隨機的感覺,出現這種情況的原因主要和setTimeout的實現代碼有關,當我們不傳時間參數或者設置為0的時候,nodejs會取值為1,即1ms(在瀏覽器端可能取值會更大一下,不同瀏覽器也各不相同),所以在電腦cpu性能夠強,能夠在1ms內執行到timers phase的情況下,由於時間延遲不滿足回調不會被執行,於是隻能等到第二輪再執行,這樣setInterval就會先執行。
可能由於cpu多次執行相同任務用時會有細微差別,而且在1ms上下浮動,才會造成上面的隨機現象
一般情況下setTimeout為0時候會在setImmediate之前執行

例子2:
當我們傳入的值大於定時器timer執行的回調時間的時候會直接導致定時器在下一次事件循環中執行

setTimeout(() => {
    console.log('===setTimeout===');
},10);
setImmediate(() => {
    console.log('===setImmediate===')
})

在這裡插入圖片描述

例子3:
當我們將上述代碼放入一個i/o中就會固定先check再而timer:

const fs = require('fs');

fs.readFile("./any.js", (data) => {
    setTimeout(() => {
        console.log('===setTimeout===');
    },10);
    setImmediate(() => {
        console.log('===setImmediate===')
    })
});

在這裡插入圖片描述

在第一輪循環中讀取文件,在回調中,會進入check階段進而執行setImmediate,隨後timer階段執行定時器。
setimmediate 與 settimeout 放入一個 I/O 循環內調用,則 setImmediate 總是被優先調用

(2) process.nextTick

這個函數其實是獨立於 Event Loop 之外的,它有一個自己的隊列,當每個階段完成後,如果存在 nextTick 隊列,就會清空隊列中的所有回調函數,並且優先於其他 microtask 執行。

例子1:

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

例子2:

const fs = require('fs');

fs.readFile("./any.js", (data) => {
    process.nextTick(()=>console.log('process===2'))
    setTimeout(() => {
        console.log('===setTimeout===');
    },10);
    setImmediate(() => {
        console.log('===setImmediate===')
    })
});
process.nextTick(()=>console.log('process===1'))

在這裡插入圖片描述

練習例子

async function async1() {
    console.log('2')
    //會等待await執行完 但是不會向下執行 因為下面輸入微任務
    await async2()
    console.log('9')
  }
   
   function async2() {
    console.log('3')
  }
   
  console.log('1')
   
  setTimeout(function () {
    console.log('11')
  }, 0)
   
  setTimeout(function () {
    console.log('13')
  }, 300)
   
  setImmediate(() => console.log('12'));
   
  process.nextTick(() => console.log('7'));
   
  async1();
   
  process.nextTick(() => console.log('8'));
   
  new Promise(function (resolve) {
    console.log('4')
    resolve();
    console.log('5')
  }).then(function () {
    console.log('10')
  })
   
  console.log('6')

分析:
上面的循序就是序號的順序;
首先打印1:
前面都是兩個函數聲明,所有直接打印1,這行同步代碼;
打印2:
打印完1後,都是異步代碼,加入異步任務隊列,直接到async1函數調用,在這個函數中打印2;
打印3:
async1這個函數是個async await函數,所有也是一個變相的同步操縱等待async2函數執行,async2執行後並不會直接打印9,原因await接受的是一個promise的then操作,所以後面屬於一個promise的回調操作屬於微任務,加入微任務隊列;
打印4:
process.nextTick為微任務,所以會繼續執行promise,打印4;
打印5:
resolve()的回調不會立即執行屬於微任務,加入微任務隊列,所以打印5;
打印6:
最後一個主線程的同步代碼,打印6;
打印7、8:
process.nextTick優先級高於其他定時器,所以會直接執行回調函數打印7、8;
打印9、10:
這個時候需要執行微任務隊列中的微任務,目前有兩個9和10,按照先後循序,先打印9後打印10;
打印11、12:
setTimeout為0秒比setImmediate執行早,按照先後循序,先打印11後打印12;
打印13:
setTimeout為300ms的函數,打印13;

例子:

async function async1() {
    console.log('2')
    //會等待await執行完 但是不會向下執行 因為下面輸入微任務
    await async2()
    console.log('9')
  }
   
   function async2() {
    console.log('3')
  }
   
  console.log('1')
   
  setTimeout(function () {
    console.log('11')
    setTimeout(() => {
        console.log('11-1');
    },100);
    setImmediate(() => {
        console.log('11-2')
    })
  }, 0)
   
  setTimeout(function () {
    console.log('13')
    setTimeout(() => {
        console.log('15');
    },10);
    setImmediate(() => {
        console.log('14')
    })
  }, 300)
  setImmediate(() => console.log('12'));
  process.nextTick(() => console.log('7'));
  async1();
   
  process.nextTick(() => console.log('8'));
   
  new Promise(function (resolve) {
    console.log('4')
    resolve();
    console.log('5')
  }).then(function () {
    console.log('10')
  })
   
  console.log('6')

總結:

到此這篇關於node事件循環中事件執行的順序的文章就介紹到這瞭,更多相關node 事件執行的順序內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

參考:https://www.cnblogs.com/everlose/p/12846375.html

推薦閱讀: