深入理解Node.js-背景瞭解

張倩qianniuer發表於2018-07-01

標籤: node 原始碼 學習


原始碼: nianniuer


node背景,瞭解一下

(1)體系架構

Node.js主要分為四大部分,Node Standard Library,Node Bindings,V8,Libuv,架構圖如下:

cmd-markdown-logo

  • Node Standard Library 是我們每天都在用的標準庫,如Http, Buffer 模組。
  • Node Bindings 是溝通JS 和 C++的橋樑,封裝V8和Libuv的細節,向上層提供基礎API服務。
  • 這一層是支撐 Node.js 執行的關鍵,由 C/C++ 實現。
    • V8 是Google開發的JavaScript引擎,提供JavaScript執行環境,可以說它就是 Node.js 的發動機。
    • Libuv 是專門為Node.js開發的一個封裝庫,提供跨平臺的非同步I/O能力
    • C-ares:提供了非同步處理 DNS 相關的能力。
    • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、資料壓縮等其他的能力。

程式碼結構

樹形結構檢視,使用 tree 命令

➜  nodejs git:(master) tree -L 1
.
├── AUTHORS
├── BSDmakefile
├── BUILDING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── GOVERNANCE.md
├── LICENSE
├── Makefile
├── README.md
├── ROADMAP.md
├── WORKING_GROUPS.md
├── android-configure
├── benchmark
├── common.gypi
├── config.gypi
├── config.mk
├── configure
├── deps
├── doc
├── icu_config.gypi
├── lib
├── node.gyp
├── out
├── src
├── test
├── tools
└── vcbuild.bat
複製程式碼

進一步檢視 deps目錄:

➜  nodejs git:(master) tree deps -L 1
deps
├── cares
├── gtest
├── http_parser
├── npm
├── openssl
├── uv
├── v8
└── zlib
複製程式碼

node.js核心依賴六個第三方模組。其中核心模組 http_parser, uv, v8這三個模組在後續章節我們會陸續展開。 gtest是C/C++單元測試框架。

(2)libuv

node.js最初開始於2009年,是一個可以讓Javascript程式碼離開瀏覽器的執行環境也可以執行的專案。 node.js使用了Google的V8解析引擎和Marc Lehmann的libev。Node.js將事件驅動的I/O模型與適合該模型的程式語言(Javascript)融合在了一起。隨著node.js的日益流行,node.js需要同時支援windows, 但是libev只能在Unix環境下執行。Windows 平臺上與kqueue(FreeBSD)或者(e)poll(Linux)等核心事件通知相應的機制是IOCP。libuv提供了一個跨平臺的抽象,由平臺決定使用libev或IOCP。在node-v0.9.0版本中,libuv移除了libev的內容。

libuv是一個高效能的,事件驅動的I/O庫,並且提供了跨平臺(如windows, linux)的API。 隨著libuv的日益成熟,它成為了擁有卓越效能的系統程式設計庫。除了node.js以外,包括Mozilla的Rust程式語言,和許多的語言都開始使用libuv。 libuv的官網:docs.libuv.org/en/v1.x/,英文…

(3)V8 concept[概念]

架構圖

cmd-markdown-logo

現在 JS 引擎的執行過程大致是:原始碼 --->抽象語法樹 --->位元組碼 --->JIT--->原生程式碼。一段程式碼的抽象語法樹示例如下:

function demo(name) {
    console.log(name);
}
複製程式碼

V8 更加直接的將抽象語法樹通過 JIT 技術轉換成原生程式碼,放棄了在位元組碼階段可以進行的一些效能優化,但保證了執行速度。 在 V8 生成原生程式碼後,也會通過 Profiler 採集一些資訊,來優化原生程式碼。雖然,少了生成位元組碼這一階段的效能優化, 但極大減少了轉換時間。

JIT就是即時編譯器,可以根據位元組碼的使用頻率對常用的位元組碼生成本地機器指令(執行時),並且儲存下來

PS:Tuborfan 將逐步取代 Crankshaft

隨著Web相關技術的發展,JavaScript所要承擔的工作也越來越多,早就超越了“表單驗證”的範疇,這就更需要快速的解析和執行JavaScript指令碼。V8引擎就是為解決這一問題而生,在node中也是採用該引擎來解析JavaScript。

Node,開始學習!

1. Node能夠解決什麼問題?

應用場景:Node的首要目標是提供一種簡單的,用於建立高效能伺服器的開發工具 Web伺服器的瓶頸在於併發的使用者量,對比Java和Php的實現方式

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

2. Node是什麼?

  • Node.js 是一個基於 Chrome V8引擎的 JavaScript 執行環境,讓JavaScript的執行效率與低端的C語言的相近的執行效率。
  • Node.js 使用了一個事件驅動非阻塞式 I/O 的模型,使其輕量又高效。
  • Node.js 的包管理器npm,是全球最大的開源庫生態系統。

3. 程式與執行緒

程式是作業系統分配資源和排程任務的基本單位,執行緒是建立在程式上的一次程式執行單位,一個程式上可以有多個執行緒。

為什麼JavaScript是單執行緒?
  • 這是由 Javascript 這門指令碼語言的用途決定的。
  • Web Worker並沒有改變 JavaScript 單執行緒的本質。
    // websocket
    let worker  = new Worker('./1.worker.js');
    worker.postMessage(10000);
    worker.onmessage = function (e) {
      console.log(e.data);
    }
    console.log('main thread')
複製程式碼
3.1 談談瀏覽器

cmd-markdown-logo

  • 使用者介面-包括位址列、前進/後退按鈕、書籤選單等
  • 瀏覽器引擎-在使用者介面和呈現引擎之間傳送指令(瀏覽器的主程式)
  • 渲染引擎,也被稱為瀏覽器核心(瀏覽器渲染程式)
  • 一個外掛對應一個程式(第三方外掛程式)
  • GPU提高網頁瀏覽的體驗(GPU程式)

由此可見瀏覽器是多程式的,並且從我們的角度來看我們更加關心瀏覽器渲染引擎

3.2 渲染引擎

渲染引擎內部是多執行緒的,內部包含兩個最為重要的執行緒ui執行緒和js執行緒。這裡要特別注意ui執行緒和js執行緒是互斥的,因為JS執行結果會影響到ui執行緒的結果。ui更新會被儲存在佇列中等到js執行緒空閒時立即被執行.


3.3 js單執行緒

javascript在最初設計時設計成了單執行緒,為什麼不是多執行緒呢?如果多個執行緒同時操作DOM那豈不會很混亂?這裡所謂的單執行緒指的是主執行緒是單執行緒的(node其實也是多執行緒的),所以在Node中主執行緒依舊是單執行緒的。

  • js執行緒和ui執行緒 是共享執行緒
  • 不能兩個執行緒,操作同一個DOM

什麼是多執行緒?
  • 同步
  • 每次請求都會產生一個執行緒,可能會遇到同時操作一個檔案,會給檔案加鎖
  • 可以開一個工作執行緒,但是歸主執行緒所管理
  • 需要切換上下文執行
  • 運用執行緒池可以優化多執行緒

什麼是單執行緒?
  • 非同步
  • 不會佔用過多記憶體,並且不需要在切換執行上下文

什麼是程式?
  • 程式是系統分配任務和排程任務的基本單位
  • 多程式 可能會遇到同時操作一個檔案會給檔案加鎖
3.4 其他執行緒
  • 瀏覽器事件觸發執行緒(用來控制事件迴圈,存放setTimeout、瀏覽器事件、ajax的回撥函式)
  • 定時觸發器執行緒(setTimeout定時器所線上程)
  • 非同步HTTP請求執行緒(ajax請求執行緒)

單執行緒不需要管鎖的問題,這裡簡單說下鎖的概念。例如大家都要去上廁所,廁所就一個,相當於所有人都要訪問同一個資源。那麼先進去的就要上鎖。而對於node來說。就一個人去廁所,所以免除了鎖的問題!


3.5. 瀏覽器中的Event Loop

主執行緒從任務佇列中讀取事件,這個過程是迴圈不斷的,所以整個的這種執行機制又稱為Event Loop(事件迴圈)

cmd-markdown-logo

  1. V8引擎解析JavaScript指令碼。
  2. 解析後的程式碼,呼叫Node API。
  3. libuv庫負責Node API的執行。它將不同的任務分配給不同的執行緒,形成一個Event Loop(事件迴圈),以非同步的方式將任務的執行結果返回給V8引擎。
  4. V8引擎再將結果返回給使用者。
queue stack
  • queue佇列: 先進先出 push shift
  • stack棧: 函式執行

3.6 Node.js的Event Loop

cmd-markdown-logo


3.7. 同步與非同步

同步和非同步關注的是訊息通知機制

  • 同步就是發出呼叫後,沒有得到結果之前,該呼叫不返回,一旦呼叫返回,就得到返回值了。 簡而言之就是呼叫者主動等待這個呼叫的結果。
  • 而非同步則相反,呼叫者在發出呼叫後這個呼叫就直接返回了,所以沒有返回結果。換句話說當一個非同步過程呼叫發出後,呼叫者不會立刻得到結果,而是呼叫發出後,被呼叫者通過狀態、通知或回撥函式處理這個呼叫。

補充知識:巨集任務 && 微任務(vue $nextTick) 非同步方法

// 巨集任務 setTimeout
// 同步程式碼執行後才會執行非同步
// 根據時間排序,當時間到達後把對應的回撥放到佇列
    setTimeout(() => {
      console.log(1);
      setTimeout(() => {
        console.log(4);
      }, 1000);
    }, 1000);
    setTimeout(() => {
      console.log(2);
    }, 2000);
    setTimeout(() => {
      console.log(3);
    }, 3000);
複製程式碼
    // 巨集任務 setImmedate
    // setImmedate只相容ie,預設是低於 setTimeout
    setImmediate(function () {
      console.log('setImmediate')
    })
    setTimeout(function () {
      console.log('timeout')
    }, 4);
    console.log(1);
複製程式碼
    // 巨集任務 MessageChannel
    let messageChannel = new MessageChannel();
    let prot2 = messageChannel.port2;
    // postMessage是非同步執行的,要等待同步都執行完後才會被呼叫
    messageChannel.port1.postMessage('111');
    console.log(1);
    prot2.onmessage = function (e) {
      console.log(e.data);
    }
    console.log(2);
複製程式碼
     // 巨集任務 MutationObserver廢棄了,相容/效能有問題
     let observe = new MutationObserver(function () {
        alert('已經dom更新好了')
     });
     observe.observe(app,{childList:true});
     for(let i = 0;i<1000;i++){
        app.appendChild(document.createElement('span'));
    }
複製程式碼

3.8. 阻塞與非阻塞I/O

阻塞和非阻塞關注的是程式在等待呼叫結果(訊息,返回值)時的狀態.

  • 阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起。呼叫執行緒只有在得到結果之後才會返回。
  • 非阻塞呼叫指在不能立刻得到結果之前,該呼叫不會阻塞當前執行緒。

3.9. 組合

同步非同步取決於被呼叫者,阻塞非阻塞取決於呼叫者

  • 同步阻塞
  • 非同步阻塞
  • 同步非阻塞
  • 非同步非阻塞

4. 什麼場合下應該考慮使用Node框架?

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

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

相關文章