Deno 並不是下一代 Node.js

justjavac發表於2018-06-04

這幾天前端圈最火的事件莫過於 ry(Ryan Dahl) 的新專案 deno 了,很多 IT 新聞和媒體都用了標題:“下一代 Node.js”。這週末讀了一遍 deno 的原始碼,特意寫了這篇文章。長文預警(5000字,11圖)。

0. 為什麼開發 Deno?

這是我上週做的一張圖,介紹了 JavaScript 的發展簡史。剛才修改了一下,新增了對 Node.js 和 Deno 釋出時間的標註。 Node.js 和 Deno 分別是 Ryan Dahl 在 2009 年和 2018 年,基於當年最新的前端技術開發的非瀏覽器 JavaScript 執行時

JavaScript 歷史(Node & Deno)

Ryan Dahl 開發 deno 並不是因為 “just for fun”,也不是為了取代 node。下面慢慢解釋。

1. 目前 deno 只是一個 demo

這兩天花時間看了 deno 的原始碼(好在是初級階段,原始碼很少,也很容易理解),順帶看了所有的 issue 和 pr。不知道“從官方介紹來看,可以認為它是下一代 Node”是如何腦補出來的。

既然是 Node.js 之父的新作,在討論中自然離不開 Node.js。而作者很皮的回覆到:

The main difference is that Node works and Deno does not work : )

最大的區別就是:Node 可以工作,而 Deno 不行 : )

目前 Deno 只是一個 Demo,甚至連二進位制發行版都沒有。好在從原始碼編譯比較簡單(如果你使用的不是 Windows 系統)。

在 high-level 層面,Deno 提供了一個儘可能簡單的 V8 到系統 API 的繫結。為什麼使用 Golang 替代 C++ 呢,因為相比 Node 而言,Golang 讓我們更加容易的新增新特性,比如 http2 等。

至於為什麼不選擇 Rust,作者沒有回答。

我們再對比一下兩者的啟動效能。分別執行:

console.log('Hello world')
複製程式碼

node vs deno

我之前寫過一篇文章:Node.js 新計劃:使用 V8 snapshot 將啟動速度提升 8 倍,那如果我們使用 --without-snapshot 引數編譯 Node.js 呢?

deno vs node(without-snapshot)

依然是相差懸殊,畢竟 deno 需要載入一個 TypeScript 編譯器。畢竟是一個 demo 版本,希望以後用力優化。

對於效能提升還有一個思路就是,可以使用 LLVM 作為後端編譯器把 TypeScript 程式碼編譯為 WebAssembly 然後在 V8 裡面執行,甚至可以直接把原始碼編譯成二進位制程式碼執行。Ryan Dahl 表示 deno 只需要一個編譯器,那就是 TS。但是既然 deno 要相容瀏覽器,那麼 WebAssembly 應該也會被支援。

Deno 可以對 ts 的編譯結果進行快取(~/.deno/cache),所以目前關注的就是啟動速度和初次編譯速度。

要麼就是在釋出前先行編譯,如此一來 deno 就脫離了開發的初衷了。deno 是一個 ts 的執行時,那麼就應該可以直接執行 ts 程式碼,如果提前把 ts 編譯成 js,那麼 deno 就回退到 js 執行時了。

2. 初學者應該學習 Node.js 還是 Deno?

對於這個問題,Ryan Dahl 的回答乾淨利落:

Use Node. Deno is a prototype / experiment.

使用 Node。Deno 只是一個原型或實驗性產品。

從介紹可以看到,Deno 的目標是不相容 Node,而是相容瀏覽器。

所以,Deno 不是要取代 Node.js,也不是下一代 Node.js,也不是要放棄 npm 重建 Node 生態。deno 的目前是要擁抱瀏覽器生態。

不得不說這個目標真偉大。Ryan Dahl 開發了 Node.js,社群構建出了整個 npm 生態。我在另一個回答 justjavac:純前端開發眼裡nodejs到底是什麼? 裡面寫到“Node.js 是前端工程化的重要支柱之一”。

雖然後來 Ryan Dahl 離開 Node.js 去了 Golang 社群,但是現在 Ryan Dahl 又回來了,為 JavaScript 社群帶來了 Golang,開發出了 Deno,然後擁抱瀏覽器生態。?

我們看看 deno 的關於 Web API 的目標:

  • High level
    • Console √
    • File/FileList/FileReader/Blob
    • XMLHttpRequest
    • WebSocket
  • Middle level
    • AudioContext/AudioBuffer
    • Canvas

甚至還會包括 webGL 和 GPU 等的支援。

3. Deno 的架構

Parsa Ghadimi 繪製了一張關於 Deno 的架構圖

Deno‘s architecture

底層使用了作者開發的 v8worker2,而 event-loop 則基於 pub/sub 模型。關於 v8worker 可以看看這個 PPT:docs.google.com/presentatio…

我比較好奇的是 deno 使用了 protobuf,而沒有使用 Mojo。既然目標是要相容瀏覽器,卻不使用 Mojo,而是要在 protobuf 上重新造輪子,可見 Ryan Dahl 是真正的“輪子哥”啊。如果想要相容瀏覽器生態,選擇 Mojo 是個捷徑,而如果目標是高效能的伺服器,那麼應該選擇非序列化的 zero-copy 庫。無論從哪個角度看 protobuf 好像都不太適合 deno。但是從 issue 中可以看出,Ryan Dahl 之前是沒有聽說過 Mojo 的,但是他看完 mojo 之後,依然覺得 protobuf 的選擇是正確的。

Mojo 是 Google 開發的新一代 IPC 機制,用以替換舊的 Chrome IPC。目前 Chrome 的最新版本是 67,而 Google 的計劃是在 2019 年的 75 版本用 mojo 替換掉所有的舊的 IPC。

Mojo 的思路確實和 protobuf 畢竟像,畢竟都是 Google 家的。舊的 IPC 系統是基於在 2 個程式(執行緒)之間的命名管道(IPC::Channel)實現的。這個管道是一個佇列,程式間的 IPC 訊息按照先進先出的順序依次傳遞,所以不同的 IPC 訊息之間有先後次序的依賴。相比之下,Mojo 則為每一個介面建立了一個獨立的訊息管道,確保不同介面的 IPC 是獨立的。而且為介面的建立獨立的訊息管道的代價也並不昂貴,只需分配少量的堆記憶體。

Mojo 的架構設計:

Deno 並不是下一代 Node.js

我們可以看一下 Chrome 引入 Mojo 之後的架構變化。

之前:

Deno 並不是下一代 Node.js

之後:

Deno 並不是下一代 Node.js

是不是有點微服務的感覺。

熟悉 Java 的 Spring 的可以明顯看出這個依賴倒置。Blink 本來是瀏覽器最底層的排版引擎,通過 Mojo,Blink 變成了要給中間模組。最近大熱的 Flutter 也是基於 Mojo 架構的。

4. TypeScript VS JavaScript

deno 的介紹是一個安全的 TypeScript 執行環境。但是我們看原始碼就會發現,deno 整合進了一個 TypeScript 編譯器,而入口檔案中 ry/deno:main.go

// It's up to library users to call 
// deno.Eval("deno_main.js", "denoMain()") 
func Eval(filename string, code string) { 
    err := worker.Load(filename, code) 
    exitOnError(err) 
} // It's up to library users to call
// deno.Eval("deno_main.js", "denoMain()")
func Eval(filename string, code string) {
    err := worker.Load(filename, code)
    exitOnError(err)
}
複製程式碼

使用 V8 執行的 deno_main.js 檔案。是 JavaScript 而不是 TypeScript 。

在前面的分析中我們知道這會影響 deno 的初次啟動速度。那麼對於執行速度呢?從理論上,TypeScript 作為一種靜態型別語言,編譯完成的 JavaScript 程式碼會有更快的執行速度。我之前在《前端程式設計師應該懂點V8 知識》曾經提到過 V8 對於 JavaScript 效能提升有一項是 Type feedback

當 V8 執行一個函式時,會基於函式傳入的實參(注意是實參,而不是形參,因為 JavaScript 的形參是沒有型別的)進行即時編譯(JIT):

Deno 並不是下一代 Node.js

但是當後面再次以不同的型別呼叫函式時,V8 會進行**去優化(Deopt)**操作。

(將之前優化完的結果去掉,稱為“去優化”)

Deno 並不是下一代 Node.js

但是如果我們使用 TypeScript ,所有的引數都是由型別標註的,因此可以防止 V8 引擎內部執行去優化操作。

5. 對 deno 效能的展望和猜想

雖然 TypeScript 可以避免 V8 引擎的去優化操作,但是 V8 執行的是 ts 編譯後的結果,我們通過位元組碼或者機器碼可以看到,V8 依然生成了 Type Check 的程式碼,每次呼叫函式之前,V8 都會對實參的型別進行檢查。也就是說,雖然 TypeScript 保證了函式的引數型別,但是編譯成 JavaScript 之後,V8 並不能確定函式的引數型別,只能通過每次呼叫前的檢查來保證引數的型別。

其次,當 V8 遇到函式定義時,並不知道引數的型別,而只有函式被呼叫後,V8 才能判斷函式的型別,才對函式進行 Typed 即時編譯。這裡又有一個矛盾了,typescript 在函式定義時就已經知道了形參的型別,而 V8 只有在函式呼叫時才根據實參的型別進行優化。

所以,目前 deno 的架構還存在很多問題,畢竟只是一個 demo。未來還有很多方向可以優化。

V8 是一個 JavaScript 執行時,而 deno 如果定義為“安全的 TypeScript 執行時”,至少在目前的架構上,效能是有很大損失的。但是目前還不存在一個 TypeScript 執行時,退而求其次只能在 V8 前面放一個 TypeScript 編譯器了。

執行流程是這樣的:

Deno 並不是下一代 Node.js

Deno 並不是下一代 Node.js

雖然我在專案中沒有使用過 TypeScript ,但是基本上我在專案裡面寫的第三方庫都會提供一d.ts 檔案。目前 TypeScript 最大的用途還是體現在開發和維護過程中。

我們想到的一個方式就是 fork 一份 V8 的原始碼,然後把編譯流程整合進去。TypeScript 在編譯為 JavaScript 的過程中也需要一份 AST,然後生成 js 程式碼。V8 執行 js 程式碼是再 parse 一份 AST,基於 AST 生成中間程式碼(ByteCode)。如果 TypeScript 可以直接生成對用的位元組碼則會提升執行時的效能。

不過 Ryan Dahl 大概不會這麼幹。但是也未必,畢竟社群已經把 TypeScript 的一個子集編譯為 WebAssembly 了。

之前微軟的 JScript 和 VBScript 在和 JavaScript 的競爭中敗下陣來,而現在 TypeScript 勢頭正猛。雖然對 ES 規範的相容束縛了 TypeScript 的發展,但很期待微軟可以提供一個 TS 執行時,或者在 Chakra 引擎增加對 TS 執行時的支援。

6. 總結

不論如何,deno 是一個非常偉大的專案,但卻不是“下一代 Node.js”。


PS:昨天 Ryan Dahl 在 JS Conf 做了《Design Mistakes in Node》的演講,目前只有 PPT,還沒有 Youtube 視訊。而 8 年前的 2009 年,Ryan Dahl 也在 JS Conf 做了一次演講,這次演講誕生了 Node.js。

相關文章