初窺JavaScript事件機制的實現(一)—— Node.js事件驅動實現概覽

在瀏覽器中,事件做爲一個極爲重要的機制,給予JavaScript響應用戶操做與DOM變化的能力;在Node.js中,事件驅動模型則是其高併發能力的基礎。html

學習JavaScript也須要了解它的運行平臺,爲了更好的理解JavaScript的事件模型,我打算從Node及瀏覽器引擎源碼入手,分析其底層實現,並將個人分析整理爲一系列博文;一方面做爲筆記,另外一方面也但願能與你們交流,分析和理解有疏漏偏頗之處,還望各位斧正。node


簡述事件驅動模型

解釋JavaScript事件模型自己的好文章已經不少了,能夠說這已是一個說爛了的話題,這裏我只簡單寫一下,而且提供一些好文章的連接。segmentfault

程序如何響應事件

咱們的程序響應外部的事件有以下兩種方式:windows

  • 中斷
    操做系統處理鍵盤等硬件輸入就是經過中斷來進行的,這個方式的好處是即便沒有多線程,咱們也能夠放心地執行咱們的代碼,CPU收到中斷信號以後自動地轉去執行相應的中斷處理程序,處理完成後會恢復原來的代碼的執行環境繼續執行。這種方式須要硬件的支持,通常來講都會被操做系統封裝起來。api

  • 輪詢
    循環檢測是否有事件發生,若是有就去執行相應的處理程序。這在底層和上層的開發中都有應用。
    Windows窗口程序就須要在主線程中寫下以下代碼,一般稱作消息循環:瀏覽器

    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    消息循環不斷檢測是否有消息(用戶的UI操做、系統消息等)出現,有的話就分發消息,調用相應的回調函數進行處理。
    輪詢方式的一個缺點就是:若是在主線程的消息循環裏進行耗時操做,程序就沒法及時響應新的消息。這在JavaScript中表現明顯,之後還會提到這一點,並探討其解決方案。多線程

    然而JavaScript中並無相似消息循環代碼,咱們只是簡單地註冊事件,而後等待被調用。這是由於瀏覽器、Node做爲執行平臺,已經將event loop實現了,JavaScript代碼不須要介入到這個過程當中,只須要做爲被調用者安靜地等待便可。架構

相關討論

  1. 知乎-關於瀏覽器處理事件的問題?匿名用戶的回答:這個回答裏圖很不錯,有助於理解event loop的工做原理;答案末尾有一些文章分享;併發

  2. MDN - Concurrency model and Event Loop:MDN上對event loop的介紹。異步

Node中的event loop

經過Node源碼看event loop的實現

Node採用V8做爲JavaScript的執行引擎,同時使用libuv實現事件驅動式異步I/O。其事件循環就是採用了libuv的默認事件循環。

在src/node.cc中,

Environment* env = CreateEnvironment(
        node_isolate,
        uv_default_loop(),
        context,
        argc,
        argv,
        exec_argc,
        exec_argv);

這段代碼創建了一個node執行環境,能夠看到第三行的uv_default_loop(),這是libuv庫中的一個函數,它會初始化uv庫自己以及其中的default_loop_struct,並返回一個指向它的指針default_loop_ptr
以後,Node會載入執行環境並完成一些設置操做,而後啓動event loop:

bool more;
do {
  more = uv_run(env->event_loop(), UV_RUN_ONCE);
  if (more == false) {
    EmitBeforeExit(env);
    // Emit `beforeExit` if the loop became alive either after emitting
    // event, or after running some callbacks.
    more = uv_loop_alive(env->event_loop());
    if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
      more = true;
  }
} while (more == true);
code = EmitExit(env);
RunAtExit(env);
...

more用來標識是否進行下一輪循環。
env->event_loop()會返回以前保存在env中的default_loop_ptruv_run函數將以指定的UV_RUN_ONCE模式啓動libuv的event loop。在這種模式下,uv_run會至少處理一個事件:這意味着,若是當前事件隊列中沒有須要處理的I/O事件,uv_run會阻塞住,直到有I/O事件須要處理,或者下一個定時器時間到。若是當前沒有I/O事件也沒有定時器事件,則uv_run返回false。

接下來Node會根據more的狀況決定下一步操做:

  • 若是moretrue,則繼續運行下一輪loop

  • 若是morefalse,說明已經沒有等待處理的事件了,EmitBeforeExit(env);觸發進程的'beforeExit'事件,檢查並處理相應的處理函數,完成後直接跳出循環。

最後觸發'exit'事件,執行相應的回調函數,Node運行結束,後面會進行一些資源釋放操做。

在libuv中,event loop會在每次循環的開始更新本身的time從而實現計時功能,而I/O事件則分爲兩類:

  • Network I/O是使用系統提供的非阻塞式I/O解決方案,例如在Linux上使用epoll,windows上使用IOCP。

  • 文件操做和DNS操做沒有(很好的)系統解決方案,所以libuv自建了線程池,在其中進行阻塞式I/O。

另外咱們也能夠將自定義的函數拋到線程池中運行,在運行結束後主線程會執行相應的回調函數,不過Node並無將這一項功能加入到JavaScript中,也就是說只用原生Node是沒法在JavaScript中開啓新的線程進行並行執行的。

相關資料

  1. libuv Design Overview:關於libuv的架構及設計思路;

  2. node child_process 'exit':Node的child_process 'exit'事件;

  3. Node with threads:討論了使用libuv線程池異步運行JavaScript代碼。

下一篇: 初窺JavaScript事件機制的實現(二)—— Node.js中定時器的實現

相關文章
相關標籤/搜索