《進擊的前端工程師》-Node.js事件循環

本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或從新修改使用,但需註明來源。署名 4.0 國際 (CC BY 4.0)前端

觀感度:🌟🌟🌟🌟🌟node

口味:法式鵝肝linux

烹飪時間:20mingit

事件循環

事件循環的執行順序從圖中能夠看出,每次的事件循環都包含了上圖中的6個階段,接下來咱們來一一解讀它們。github

timers 定時器

計時器分爲兩類:web

  • Immediate 在下一個check階段執行
  • Timeout 定時器過時後執行(delay參數默認值爲1ms)

Timeout計時器又有兩種類型:瀏覽器

  • Interval
  • Timeout

這個階段會執行setTimeout()和setInterval()設定的回調前端工程師

timers的執行是由poll階段控制的異步

setTimeout()和setInterval()和瀏覽器中的API是相同的。它們的實現原理與異步I/O比較相似,可是不須要I/O線程池的參與。socket

這兩個定時器建立後會被插入到定時器觀察者內部的一個紅黑樹中。每次Tick執行時,都會從紅黑樹中取出定時器對象,來檢查它們是否超過定時時間,超過便執行它們的回調。

注意:定時器存在一個問題,就是它不是絕對精確的(在容忍範圍內)。一旦某個事件循環中,有一個任務佔用了較多的時間,那麼再次輪到定時器執行時,時間就會受到影響。

無IO處理狀況

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

經過執行上面的代碼咱們能夠發現,輸出結果是不肯定的。

由於setTimeout(fn, 0)具備幾毫秒的不肯定性,沒法保證進入timers階段,定時器能當即執行處理程序。

有IO處理狀況

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

此時setImmediate優先於setTimeout執行,由於poll階段執行完成後進入check階段,而timers階段則處於下一個事件循環階段了。

pending callbacks 待定回調

執行大部分回調,除了close,times和setImmediate()設定的回調

idle,perpare

僅供內部使用

poll 輪詢

獲取新的I/O事件,在適當的條件下,Node.js會在這裏阻塞

這個階段的主要任務是執行到達delay時間的timers定時器的回調,而且處理poll隊列裏的事件。

當事件循環進入poll階段,而且沒有調用定時器時,將會發生如下兩種狀況:

1.若是poll隊列不爲空,事件循環將遍歷同步執行它們的回調隊列。

2.若是poll隊列爲空,又分爲兩種狀況:

  • 若是被setImmediate()回調調用,事件循環會結束poll階段,進入到check階段。

  • 若是沒有被setImmediate()回調調用,事件循環將阻塞並等待回調添加到poll隊列中執行。

一旦poll隊列爲空,事件循環將查看計時器是否到達delay時間,若是一個或多個定時器已達到delay時間,事件循環將回滾到timers定時器階段,執行它們的回調。

check 檢測

setImmediate()設定的回調會在這一階段執行

如同上文poll階段的第二種狀況中,若是poll隊列爲空,而且被setImmediate()回調調用,事件循環將直接進入check階段。

close callbacks 關閉的回調函數

socket.on('close',callback)的回調會在這個階段執行

libuv

libuv爲Node.js提供了整個事件循環功能。

如上圖所示,在Windows下,事件循環基於IOCP建立,在linux下經過epoll實現,FreeBSD下經過kqueue實現,在Solaris下經過Event ports實現。

咱們再細心的去看上圖,Network I/O和file I/O、DNS等實現方式是被分隔開的,這是由於他們的本質是由兩套機制來實現的。咱們一下子來經過源碼窺探它們的本質。

實質上,當咱們寫JavaScript代碼去調用Node的核心模塊時,核心模塊會調用C++內建模塊,內建模塊經過libuv進行系統調用。

libuv主要解決的問題

在現實世界中,在全部不一樣類型的操做系統平臺下,支持不一樣類型的I/O是很是困難的。那麼爲了支持跨平臺I/O的同時,能更好的管理整個流程,抽象出了libuv。

簡單說,就是libuv抽象出一層API,能夠幫助你調用各個平臺和機器上各類系統特性,包括操做文件、監聽socket等,而你不須要了解它們的具體實現。

核心源碼解讀

核心函數uv_run

源碼

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
    int timeout;
    int r;
    int ran_pending;
    // 檢查loop中是否有異步任務,沒有就結束。
    r = uv__loop_alive(loop);
    if (!r)
      uv__update_time(loop);
    // 事件循環while
    while (r != 0 && loop->stop_flag == 0) {
        // 更新事件階段
        uv__update_time(loop);  
        // 處理timer回調
        uv__run_timers(loop);        
        // 處理異步任務回調
        ran_pending = uv__run_pending(loop);      
        // 供內部使用
        uv__run_idle(loop);
        uv__run_prepare(loop);        
        // uv_backend_timeout計算完畢後,會傳給uv__io_poll
        // 若是timeout = 0,則uv__io_poll會直接跳過
        timeout = 0;
        if ((mode == UV_RUN_ONCE && !ran_pending || mode == UV_RUN_DEFAULT))
          timeout = uv_backend_timeout(loop);
        uv__io_poll(loop, timeout);
        // check階段
        uv__run_check(loop);
        // 關閉文件描述符等操做
        uv__run_closing_handles(loop);
        // 檢查loop中是否有異步任務,沒有就結束。
        r = uv__loop_alive(loop);
        if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
          break;
    }
    return r;
}
複製代碼

事件循環的真實面目是一個while。

上文說到Network I/O與file I/O、DNS等是由兩套機制來實現的。

首先咱們來看Network I/O,它最後的調用都會歸結到uv__io_start這個函數,而該函數會將須要執行的I/O事件和回調放入watcher隊列中,而uv__io_poll階段會從watcher隊列中取出事件調用系統的接口並執行。

(uv__io_poll部分的代碼過長你們感興趣可自行查看)

uv__io_start

void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
  assert(0 == (events & ~(POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI)));
  assert(0 != events);
  assert(w->fd >= 0);
  assert(w->fd < INT_MAX);
  w->pevents |= events;
  maybe_resize(loop, w->fd + 1);
  if (w->events == w->pevents)
    return;
  if (QUEUE_EMPTY(&w->watcher_queue))
    QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue);
  if (loop->watchers[w->fd] == NULL) {
    loop->watchers[w->fd] = w;
    loop->nfds++;
  }
}
複製代碼

如上所示就是咱們libuv中Network I/O這條主線實現過程。

而另一條主線是Fs I/O和DNS等操做則會調用uv__work_sumit這個函數,這個函數是執行線程池初始化uv_queue_work中最終調用的函數。

void uv__work_submit(uv_loop_t* loop, struct uv__work* w, enum uv__work_kind kind, void (*work)(struct uv__work* w), void (*done)(struct uv__work* w, int status)) {
  uv_once(&once, init_once);
  w->loop = loop;
  w->work = work;
  w->done = done;
  post(&w->wq, kind);
}
複製代碼
int uv_queue_work(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb) {
  if (work_cb == NULL)
    return UV_EINVAL;
  uv__req_init(loop, req, UV_WORK);
  req->loop = loop;
  req->work_cb = work_cb;
  req->after_work_cb = after_work_cb;
  uv__work_submit(loop,
                  &req->work_req,
                  UV__WORK_CPU,
                  uv__queue_work,
                  uv__queue_done);
  return 0;
}
複製代碼

Node.js中的事件隊列

Node.js中有多個隊列,不一樣類型的事件在各自的隊列中排隊。在一個階段結束後,進入下一個階段以前,事件循環會在這中間處理中間隊列。

原生的libuv事件循環中的隊列主要又4種類型:

  • 過時的定時器和間隔隊列

  • IO事件隊列

  • Immediates隊列

  • close handlers隊列

除此以外,Node.js還有兩個中間隊列

  • Next Ticks隊列

  • Other Microtasks隊列

Node.js與瀏覽器的Event Loop差別

咱們能夠回顧下瀏覽器中JavaScript事件循環,請移步個人另外一篇系列專欄《進擊的前端工程師》系列-瀏覽器中JavaScript的事件循環

回來後,先說結論:

在瀏覽器中,microtask的任務隊列是每一個macrotask執行完以後執行。

在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。

(本文的Macrotask在WHATWG 中叫task。Macrotask爲了便於理解,並無實際的出處。)

相比於瀏覽器,node多出了setImmediate(宏任務)process.nextTick(微任務)這兩種異步操做。

setImmediate的回調函數被放在check階段執行。而process.nextTick會被當作一種microtask,每一個階段結束後都會執行全部的microtask,你能夠理解爲process.nextTick能夠插隊,在下個階段前執行。

process.nextTick插隊帶來的危害

process.nextTick的回調會致使事件循環沒法進入到下一個階段。I/O處理完成或者定時器過時後仍然沒法執行。會讓其餘的事件處理程序處於飢餓狀態,爲了防止這個問題,Node.js提供了一個process.maxTickDepth(默認爲1000)。

Node.js中的微任務

  • process.nextTick()
  • Promise.then()
Promise.resolve().then(function(){
    console.log('then')
})
process.nextTick(function(){
    console.log('nextTick')
});
// nextTick
// then
複製代碼

咱們能夠看到nextTick要早於then執行。

Node.js v11變動的事件循環

從Node.js v11開始,事件循環的原理髮生了變化,在同一個階段中只要執行了macrotask就會當即執行microtask隊列,與瀏覽器表現一致。具體請參考這個pr

❤️看完三件事

1.看到這裏了就點個贊支持下吧,你的點贊是我創做的動力。

2.關注公衆號前端食堂,你的前端食堂,記得按時吃飯!

3.入冬了,多穿衣服不要着涼~!

相關文章
相關標籤/搜索