Node.js中的事件循環(Event Loop)

什麼是事件循環

衆所周知,JavaScript 是單線程的,而 Nodejs 又能夠實現無阻塞的 I/O 操做,就是由於 Event Loop 的存在。javascript

Event Loop 主要有如下幾個階段,一個矩形表明着一個階段,以下所示:java

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
複製代碼

每一個階段都會維護一個先進先出的隊列結構,當事件循環進入某個階段時,就會執行該階段的一些特定操做,而後依序執行隊列中的回調函數。 當隊列中的回調函數都執行完畢或者已執行的回調函數的個數達到某個最大值,就會進入下一個階段。node

timers

事件循環的開始階段,執行 setTimeoutsetInterval 的回調函數。git

當定時器規定的時間到了以後,就會將定時器的回調函數放入隊列中,而後依序執行。github

假如你有 a、b、c 四個定時器,時間間隔分別爲 10ms 、20ms 、 30ms。當進入事件循環的 timer 階段時,時間過去了 25 ms,那麼定時器 a 和 b 的回調就會被執行,執行完畢後就進入下一個階段。canvas

I/O callbacks

執行除了 setTimeoutsetIntervalsetImmediateclose callbacks 等的回調函數。promise

idle, prepare

進行一些內部操做。bash

poll

這應該是事件循環中最重要的一個階段了。異步

若是這個階段的隊列不爲空,那麼隊列中的回調會被順序執行;若是隊列爲空,也有 setImmediate 函數被調用,那麼就會進入 check 階段。若是隊列爲空且沒有 setImmediate 的函數調用,事件循環會進行等待,一旦有回調函數被添加到隊列中時,當即執行。socket

check

setImmediate 的回調會在這個階段被執行。

close callbacks

例如 socket.on('close', ...) 等的回調在這個階段執行。

setTimout vs setImmediate

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

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

按照以前的說法,事件循環會先進入 timer 階段,執行 setTimeout 的回調,等到進入 check 階段時, setImmediate 的回調纔會被執行。因此有一些人認爲上面的代碼的輸出結果應該是:

$ node timeout_vs_immediate_1.js
timeout
immediate
複製代碼

但其實這裏的結果是不肯定的。這裏每每跟進程的性能有關係,並且,這裏 setTimeout 的間隔雖然是 0,實際上會是 1。因此當啓動程序進入事件循環,時間還未過去 1ms 時,timer 階段的隊列是空的,不會有回調被執行。而這裏又有 setImmediate 函數的調用,因此以後走到 check 階段時,setImmediate 的回調會被調用。若是事件循環進入 timer 階段時,已經消耗了 1ms ,那麼這個時候 setTimeout 的回調就會被執行,以前進入到 check 階段,再執行 setImmediate 的回調。

因此,如下的兩種輸出均可能出現。

$ node timeout_vs_immediate_1.js
timeout
immediate

$ node timeout_vs_immediate_1.js
immediate
timeout
複製代碼

假如,上面的代碼是放在一個 I/0 循環內,如

// timeout_vs_immediate_2.js
const fs = require('fs');

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

那麼結果就是肯定的,輸出結果以下

$ node timeout_vs_immediate_2.js
immediate
timeout
複製代碼

process.nextTick()

process.nextTick() 並不屬於事件循環中的一部分,但也是異步的 API 。

當前的操做完成了以後,若是有 process.nextTick() 的調用,那麼 process.nextTick() 中的回調會接着被執行,若是回調裏面又有 process.nextTick() ,那麼回調中的 process.nextTick() 的回調也會接着被執行。因此 procee.nextTick() 可能會阻塞事件循環進入下一個階段。

// process_nexttick_1.js
let i = 0;

function foo() {
    i += 1;

    if (i > 3) return;

    console.log('foo func');

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

    process.nextTick(foo);
}

setTimeout(foo, 5);
複製代碼

按照以前的說法,上面的輸出結果以下:

$ node process_nexttick_1.js
foo func
foo func
foo func
timeout
timeout
timeout
複製代碼

你可能會有一個疑問,process.nextTick() 是在事件循環某個階段的隊列都清空以後再執行,仍是在隊列中某個回調執行完成後接着執行。想一想下面的代碼的輸出結果是什麼?

// process_nexttick_2.js
let i = 0;

function foo() {
    i += 1;

    if (i > 2) return;

    console.log('foo func');

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

    process.nextTick(foo);
}

setTimeout(foo, 2);
setTimeout(() => {
    console.log('another timeout');
}, 2);
複製代碼

執行一下,看看結果

// node version: v11.12.0
$ node process_nexttick_2.js
foo func
foo func
another timeout
timeout
timeout
複製代碼

結果如上所示,process.nextTick() 是在隊列中的某個回調完成後就接着執行的。

你看到上面的結果,有個註釋 node version: v11.12.0 。 即運行這段代碼的 node 版本爲 11.12.0 ,若是你的 node 版本是低於這個的,如 7.10.1 ,可能就會獲得不一樣的結果。

// node version: v7.10.0
$ node process_nexttick_2.js
foo func
another timeout
foo func
timeout
timeout
複製代碼

不一樣的版本表現不一樣,我想應該是新的版本作了更新調整。

process.nextTick() vs Promise

process.nextTick() 對應的是 nextTickQueue,Promise 對應的是 microTaskQueue 。

這二者都不屬於事件循環的某個部分,但它們執行的時機都是在當前的某個操做以後,那這二者的執行前後呢

// process_nexttick_vs_promise.js
let i = 0;

function foo() {
    i += 1;

    if (i > 2) return;

    console.log('foo func');

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

    Promise.resolve().then(foo);
}

setTimeout(foo, 0);

Promise.resolve().then(() => {
    console.log('promise');
});

process.nextTick(() => {
    console.log('nexttick');
});

複製代碼

運行代碼,結果以下:

$ node process_nexttick_vs_promise.js
nexttick
promise
foo func
foo func
timeout
timeout
複製代碼

若是你都搞懂了上面的輸出結果是爲什麼,那麼對於 Nodejs 中的事件循環你也就能夠掌握了。

參考資料

討論

原文地址

歡迎你們一塊兒討論,有不錯的地方請指正。

相關文章
相關標籤/搜索