Deno并不是下一代Node.js
這幾天前端圈最火的事件莫過于 ry(Ryan Dahl) 的新項(xiàng)目 deno 了,很多 IT 新聞和媒體都用了標(biāo)題:“下一代 Node.js”。這周末讀了一遍 deno 的源碼,特意寫了這篇文章。長文預(yù)警(5000字,11圖)。
0. 為什么開發(fā) Deno?
這是我上周做的一張圖,介紹了 JavaScript 的發(fā)展簡史。剛才修改了一下,添加了對(duì) Node.js 和 Deno 發(fā)布時(shí)間的標(biāo)注。
Node.js 和 Deno 分別是 Ryan Dahl 在 2009 年和 2018 年,基于當(dāng)年***的前端技術(shù)開發(fā)的非瀏覽器 JavaScript 運(yùn)行時(shí)。
Ryan Dahl 開發(fā) deno 并不是因?yàn)?“just for fun”,也不是為了取代 node。下面慢慢解釋。
1. 目前 deno 只是一個(gè) demo
這兩天花時(shí)間看了 deno 的源碼(好在是初級(jí)階段,源碼很少,也很容易理解),順帶看了所有的 issue 和 pr。不知道“從官方介紹來看,可以認(rèn)為它是下一代 Node”是如何腦補(bǔ)出來的。
既然是 Node.js 之父的新作,在討論中自然離不開 Node.js。而作者很皮的回復(fù)到:
The main difference is that Node works and Deno does not work : )
***的區(qū)別就是:Node 可以工作,而 Deno 不行 : )
目前 Deno 只是一個(gè) Demo,甚至連二進(jìn)制發(fā)行版都沒有。好在從源碼編譯比較簡單(如果你使用的不是 Windows 系統(tǒng))。
在 high-level 層面,Deno 提供了一個(gè)盡可能簡單的 V8 到系統(tǒng) API 的綁定。為什么使用 Golang 替代 C++ 呢,因?yàn)橄啾?Node 而言,Golang 讓我們更加容易的添加新特性,比如 http2 等。
至于為什么不選擇 Rust,作者沒有回答。
我們?cè)賹?duì)比一下兩者的啟動(dòng)性能。分別運(yùn)行:
- console.log('Hello world')
我之前寫過一篇文章:Node.js 新計(jì)劃:使用 V8 snapshot 將啟動(dòng)速度提升 8 倍,那如果我們使用 --without-snapshot 參數(shù)編譯 Node.js 呢?
依然是相差懸殊,畢竟 deno 需要加載一個(gè) TypeScript 編譯器。畢竟是一個(gè) demo 版本,希望以后用力優(yōu)化。
對(duì)于性能提升還有一個(gè)思路就是,可以使用 LLVM 作為后端編譯器把 TypeScript 代碼編譯為 WebAssembly 然后在 V8 里面運(yùn)行,甚至可以直接把源碼編譯成二進(jìn)制代碼運(yùn)行。Ryan Dahl 表示 deno 只需要一個(gè)編譯器,那就是 TS。但是既然 deno 要兼容瀏覽器,那么 WebAssembly 應(yīng)該也會(huì)被支持。
Deno 可以對(duì) ts 的編譯結(jié)果進(jìn)行緩存(~/.deno/cache),所以目前關(guān)注的就是啟動(dòng)速度和初次編譯速度。
要么就是在發(fā)布前先行編譯,如此一來 deno 就脫離了開發(fā)的初衷了。deno 是一個(gè) ts 的運(yùn)行時(shí),那么就應(yīng)該可以直接運(yùn)行 ts 代碼,如果提前把 ts 編譯成 js,那么 deno 就回退到 js 運(yùn)行時(shí)了。
2. 初學(xué)者應(yīng)該學(xué)習(xí) Node.js 還是 Deno?
對(duì)于這個(gè)問題,Ryan Dahl 的回答干凈利落:
Use Node. Deno is a prototype / experiment.
使用 Node。Deno 只是一個(gè)原型或?qū)嶒?yàn)性產(chǎn)品。
從介紹可以看到,Deno 的目標(biāo)是不兼容 Node,而是兼容瀏覽器。
所以,Deno 不是要取代 Node.js,也不是下一代 Node.js,也不是要放棄 npm 重建 Node 生態(tài)。deno 的目前是要擁抱瀏覽器生態(tài)。
不得不說這個(gè)目標(biāo)真?zhèn)ゴ?。Ryan Dahl 開發(fā)了 Node.js,社區(qū)構(gòu)建出了整個(gè) npm 生態(tài)。我在另一個(gè)回答 justjavac:純前端開發(fā)眼里nodejs到底是什么? 里面寫到“Node.js 是前端工程化的重要支柱之一”。
雖然后來 Ryan Dahl 離開 Node.js 去了 Golang 社區(qū),但是現(xiàn)在 Ryan Dahl 又回來了,為 JavaScript 社區(qū)帶來了 Golang,開發(fā)出了 Deno,然后擁抱瀏覽器生態(tài)。👍
我們看看 deno 的關(guān)于 Web API 的目標(biāo):
- High level
- Console √
- File/FileList/FileReader/Blob
- XMLHttpRequest
- WebSocket
- Middle level
- AudioContext/AudioBuffer
- Canvas
甚至還會(huì)包括 webGL 和 GPU 等的支持。
3. Deno 的架構(gòu)
Parsa Ghadimi 繪制了一張關(guān)于 Deno 的架構(gòu)圖:
底層使用了作者開發(fā)的 v8worker2,而 event-loop 則基于 pub/sub 模型。關(guān)于 v8worker 可以看看這個(gè) PPT:https://docs.google.com/prese...
我比較好奇的是 deno 使用了 protobuf,而沒有使用 Mojo。既然目標(biāo)是要兼容瀏覽器,卻不使用 Mojo,而是要在 protobuf 上重新造輪子,可見 Ryan Dahl 是真正的“輪子哥”啊。但是從 issue 中可以看出,Ryan Dahl 之前是沒有聽說過 Mojo 的,但是他看完 mojo 之后,依然覺得 protobuf 的選擇是正確的。
Mojo 是 Google 開發(fā)的新一代 IPC 機(jī)制,用以替換舊的 Chrome IPC。目前 Chrome 的***版本是 67,而 Google 的計(jì)劃是在 2019 年的 75 版本用 mojo 替換掉所有的舊的 IPC。
Mojo 的思路確實(shí)和 protobuf 畢竟像,畢竟都是 Google 家的。舊的 IPC 系統(tǒng)是基于在 2 個(gè)進(jìn)程(線程)之間的命名管道(IPC::Channel)實(shí)現(xiàn)的。這個(gè)管道是一個(gè)隊(duì)列,進(jìn)程間的 IPC 消息按照先進(jìn)先出的順序依次傳遞,所以不同的 IPC 消息之間有先后次序的依賴。相比之下,Mojo 則為每一個(gè)接口創(chuàng)建了一個(gè)獨(dú)立的消息管道,確保不同接口的 IPC 是獨(dú)立的。而且為接口的創(chuàng)建獨(dú)立的消息管道的代價(jià)也并不昂貴,只需分配少量的堆內(nèi)存。
Mojo 的架構(gòu)設(shè)計(jì):
我們可以看一下 Chrome 引入 Mojo 之后的架構(gòu)變化。
之前:
之后:
是不是有點(diǎn)微服務(wù)的感覺。
熟悉 Java 的 Spring 的可以明顯看出這個(gè)依賴倒置。Blink 本來是瀏覽器***層的排版引擎,通過 Mojo,Blink 變成了要給中間模塊。最近大熱的 Flutter 也是基于 Mojo 架構(gòu)的。
4. TypeScript VS JavaScript
deno 的介紹是一個(gè)安全的 TypeScript 運(yùn)行環(huán)境。但是我們看源碼就會(huì)發(fā)現(xiàn),deno 集成進(jìn)了一個(gè) 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 運(yùn)行的 deno_main.js 文件。是 JavaScript 而不是 TypeScript 。
在前面的分析中我們知道這會(huì)影響 deno 的初次啟動(dòng)速度。那么對(duì)于執(zhí)行速度呢?從理論上,TypeScript 作為一種靜態(tài)類型語言,編譯完成的 JavaScript 代碼會(huì)有更快的執(zhí)行速度。我之前在《前端程序員應(yīng)該懂點(diǎn)V8 知識(shí)》曾經(jīng)提到過 V8 對(duì)于 JavaScript 性能提升有一項(xiàng)是 Type feedback。
當(dāng) V8 執(zhí)行一個(gè)函數(shù)時(shí),會(huì)基于函數(shù)傳入的實(shí)參(注意是實(shí)參,而不是形參,因?yàn)?JavaScript 的形參是沒有類型的)進(jìn)行即時(shí)編譯(JIT):
但是當(dāng)后面再次以不同的類型調(diào)用函數(shù)時(shí),V8 會(huì)進(jìn)行去優(yōu)化(Deopt)操作。
(將之前優(yōu)化完的結(jié)果去掉,稱為“去優(yōu)化”)
但是如果我們使用 TypeScript ,所有的參數(shù)都是由類型標(biāo)注的,因此可以防止 V8 引擎內(nèi)部執(zhí)行去優(yōu)化操作。
5. 對(duì) deno 性能的展望和猜想
雖然 TypeScript 可以避免 V8 引擎的去優(yōu)化操作,但是 V8 執(zhí)行的是 ts 編譯后的結(jié)果,我們通過字節(jié)碼或者機(jī)器碼可以看到,V8 依然生成了 Type Check 的代碼,每次調(diào)用函數(shù)之前,V8 都會(huì)對(duì)實(shí)參的類型進(jìn)行檢查。也就是說,雖然 TypeScript 保證了函數(shù)的參數(shù)類型,但是編譯成 JavaScript 之后,V8 并不能確定函數(shù)的參數(shù)類型,只能通過每次調(diào)用前的檢查來保證參數(shù)的類型。
其次,當(dāng) V8 遇到函數(shù)定義時(shí),并不知道參數(shù)的類型,而只有函數(shù)被調(diào)用后,V8 才能判斷函數(shù)的類型,才對(duì)函數(shù)進(jìn)行 Typed 即時(shí)編譯。這里又有一個(gè)矛盾了,typescript 在函數(shù)定義時(shí)就已經(jīng)知道了形參的類型,而 V8 只有在函數(shù)調(diào)用時(shí)才根據(jù)實(shí)參的類型進(jìn)行優(yōu)化。
所以,目前 deno 的架構(gòu)還存在很多問題,畢竟只是一個(gè) demo。未來還有很多方向可以優(yōu)化。
V8 是一個(gè) JavaScript 運(yùn)行時(shí),而 deno 如果定義為“安全的 TypeScript 運(yùn)行時(shí)”,至少在目前的架構(gòu)上,性能是有很大損失的。但是目前還不存在一個(gè) TypeScript 運(yùn)行時(shí),退而求其次只能在 V8 前面放一個(gè) TypeScript 編譯器了。
執(zhí)行流程是這樣的:
雖然我在項(xiàng)目中沒有使用過 TypeScript ,但是基本上我在項(xiàng)目里面寫的第三方庫都會(huì)提供一d.ts 文件。目前 TypeScript ***的用途還是體現(xiàn)在開發(fā)和維護(hù)過程中。
我們想到的一個(gè)方式就是 fork 一份 V8 的源碼,然后把編譯流程整合進(jìn)去。TypeScript 在編譯為 JavaScript 的過程中也需要一份 AST,然后生成 js 代碼。V8 執(zhí)行 js 代碼是再 parse 一份 AST,基于 AST 生成中間代碼(ByteCode)。如果 TypeScript 可以直接生成對(duì)用的字節(jié)碼則會(huì)提升運(yùn)行時(shí)的性能。
不過 Ryan Dahl 大概不會(huì)這么干。但是也未必,畢竟社區(qū)已經(jīng)把 TypeScript 的一個(gè)子集編譯為 WebAssembly 了。
之前微軟的 JScript 和 VBScript 在和 JavaScript 的競爭中敗下陣來,而現(xiàn)在 TypeScript 勢(shì)頭正猛。雖然對(duì) ES 規(guī)范的兼容束縛了 TypeScript 的發(fā)展,但很期待微軟可以提供一個(gè) TS 運(yùn)行時(shí),或者在 Chakra 引擎增加對(duì) TS 運(yùn)行時(shí)的支持。
6. 總結(jié)
不論如何,deno 是一個(gè)非常偉大的項(xiàng)目,但卻不是“下一代 Node.js”。
PS:昨天 Ryan Dahl 在 JS Conf 做了《Design Mistakes in Node》的演講,目前只有 PPT,還沒有 Youtube 視頻。而 8 年前的 2009 年,Ryan Dahl 也在 JS Conf 做了一次演講,這次演講誕生了 Node.js。