唯有明晰歷史,才能了然當下,預知未來。作者從歷史角度解讀Node.js,幫助讀者透過猜忌和謠言,看清真實的Node.js,了解Node.js的核心與紅利。
令人惴惴不安的Node.js
我們越來越頻繁地看到關于JavaScript的新聞,剛開始談到的是引擎性能提升,后來則是由HTML5和Node.js帶來令人嘆為觀止的應用。如果只看表面,容易讓人產生以下各種誤解。
- Node.js的出現是讓前端工程師羞辱后端嗎?
- Node.js肯定是幾個前端工程師在實驗室里搗鼓出來的。
- 為了后端而后端,有意思嗎?
- 異步是反人類的。
- 怎么又發明了一門新語言?
- JavaScript承擔的責任太重了。
- 直覺上JavaScript不應該運行在后端。
JavaScript的誕生
JavaScript 1995年,誕生于網景公司,因為當時需要一種腳本語言協助Navigator做簡單的操作。Brendan Eich受命操刀實現它。當年網景公司與Sun合作緊密,不懂技術的高管希望得到一個類似Java的腳本語言,期望它能像Java一樣風靡。而 Brendan Eich原本期望進入網景公司做Scheme研究,卻接到了一個他不喜歡的任務。形勢所逼,他在10天的時間里完成這門語言的設計。它唯一符合高管們期望 的地方就是它長得確實像Java。
這看起來這本是一件糟糕的事,所幸Brendan Eich的才華確實令人高山仰止。高管期望的Java+Script實際上是C+Scheme+Self+Java的產物。它本不該叫 JavaScript,卻獲得了這個名字,談起這件事,許多開發者會拿“雷鋒”與“雷峰塔”的關系開玩笑。
JavaSript誕生后另一件糟糕的事出現在第一次瀏覽器大戰時。微軟在IE中實現了JScript,與JavaScript十分相似。網景為了保護自己,過早推動了這門語言標準化的進程,在1997年ECMAScript262規范頒布。
兩件事情疊加在一起,使得JavaScript還沒有足夠的經歷就被過早地出生和定型。它就像一個早產兒,之后的成長中,處處透露出不成熟的氣息。 所幸福禍相依,看起來糟糕的事情背后,依然留下了兩個拐點,為JavaScript保留了優秀的基因。
- 引入了Scheme的函數式,函數成為一等公民。函數式編程里的高階函數、偏函數都充滿了靈活性,它們在JavaScript中得以發揮。
- 引入了Self的原型鏈。得要感謝Brendan Eich的這個決定,不然現在大多數人可能只知道Java的模版繼承,卻不知道另一種繼承方式叫做“原型鏈繼承”。
最早的服務端JavaScript
很多人覺得JavaScript不適合運行在服務端,但這是錯的,盡管一直處在Java的陰影下,但網景還是期望JavaScript能夠既運行在瀏覽器 中,也能運行在服務器端。網景最早的服務端JavaScript項目叫做LiveWire,在今天的互聯網上仍能看到它的遺跡 。當JavaScript運行在服務端時,PHP還沒有盛行(PHP 最早版本于1995年發布)。
服務端JavaScript之殤
但理想與現實之間總是存在差距。瀏覽器市場份額之爭使得JavaScript在瀏覽器中固化且無可替代。另一廂,Java風行互聯網,服務端JavaScript完全沒有搶眼球的機會。并且隨著網景在瀏覽器戰爭的失利,后端JavaScript從此式微。
服務端JavaScript遺民
盡 管最初這條道路十分崎嶇,甚至荒蕪,但行人總是有的。因為這是一門圖靈完備的語言。Rhino作為Java實現的JavaScript引擎一直存在。前端 JavaScript也一直作為表單校驗功能為Web 1.0發光發熱。這一切都讓人覺得JavaScript只是一門輔助語言。這也是造成部分人誤解它的原因,認為JavaScript的能力僅限于此,在看 似職責分明的架構體系中,扮演著基本角色。后端JavaScript盡管存在,但極少有應用場景。
第二次瀏覽器大戰
直到2006年,JavaScript還處在打醬油的位置。但Web 2.0和第二次瀏覽器大戰的到來,催生了翻天覆地的變化。Web開始趨向應用化,JavaScript的角色一下子變得重要起來,從小腳本開始出現大規模編程需求。
另 一個變化則是JavaScript引擎在V8的引領下,速度大幅提升。新聞中時常出現“提升x倍”這樣的詞。這帶來了一個以往難以想象的事實——在腳本語 言中,JavaScript的速度已在頂尖之列。我的同事蘇千提供了斐波那契數列的測試結果,這是一個純考驗CPU的運算:

為了讓比較更為直觀,加入了C和Go兩個靜態語言作為參考(如表1所示)。

表1 JavaScript和其他語言性能對比
可以看到,JavaScript在V8平臺的表現與Luajit相差無幾。借助C++模塊,性能可以更進一步提升。如果需要了解最新的對比結果,可以訪問網頁。
2006-2009年,JavaScript得到了進一步發展,效果主要體現在兩個方面:
- JavaScript引擎性能繼續大幅提升;
- 趨向應用化,出現大規模編程的需求。
由于ECMAScript過早標準化,時至今日,JavaScript標準的核心其實只是其完整語言所涉及的極小部分,與DOM相關的API部分則被歸于W3C規范。
CommonJS
沉寂許久之后,服務端JavaScript開始有所復蘇。但那時JavaScript的表現還嚴重受到瀏覽器的制約,后端JavaScript也并沒有標準化,呈現出一種混亂分離的狀態。這時CommonJS出現了。
CommonJS基于語言現狀和一直想要充分發揮JavaScript能力的愿望,開始制定JavaScript超出ECMAScript262、瀏覽器DOM與BOM API之外的規范。這些規范包括模塊、包、二進制、流、Buffer、網絡等。
這 是一個理想主義的做法,但直到Node.js出現,很少聽聞它的消息。原因還在于人們的成見,認為JavaScript除了運行在瀏覽器中,沒有其他作 為。它致力于將JavaScript作為命令行工具、桌面應用、服務甚至生態系統的理想,盡管在少部分人的幫助下得以實現,但在推廣中收效甚微。
理想很美好,現實很骨感,但這還是埋下了又一伏筆,對于未來的發展至關重要:
CommonJS規范的出現可以指導JavaScript實現大規模編程。
這套規范后來不僅影響到了服務端JavaScript,也影響到前端JavaScript。
Node.js的逆襲
Node.js的出現終于讓開發者開始關注服務端JavaScript,不過Node.js的作者并不是前端工程師,按時髦話來說,Ryan Dahl是一枚資深C/C++碼農。
他 沒有幫助前端工程師羞辱后端的愿望,也沒有這份耐心。選擇JavaScript,屬于偶然中的必然。Ryan Dahl最初利用Ruby寫了一個叫做Ebb的Web服務器。像其他資深C/C++碼農一樣,他將心思花費在服務器的實現上,例如放棄原來的Apache fork/prefork、多線程等模式,因為大家都意識到單線程異步的方式比原來的多線程或多進程具備更高的性能,Nginx就是這樣的產物。此 外,Ryan Dahl還意識到,盡管面向客戶端連接可以通過異步來提升性能,但在業務層面的一個同步I/O常讓他在提升性能上所做的努力徒勞無功,因此他要的是面向客 戶端的異步,面向后端業務,也是異步的一個服務器。
他開始評估幾門語言實現這個Web服務器的可能——Ruby因為VM的性能原因被舍棄 了;盡管Ryan Dahl擅長C/C++,但他深知開發C/C++的門檻十分高。排除幾種方案后,Lua是最接近他期望的語言。但Lua已有的I/O庫是同步的,他知道, 如果在Lua中推行異步I/O,將會因為不符合人們的習慣而被拋棄。
這時JavaScript出現在了他的面前。也許是歷史的必然(或巧 合),前文描述的伏筆或多或少影響了Ryan Dahl。JavaScript的單線程、事件驅動的方式也十分貼合Ryan Dahl的理念。更重要的是,異步在JavaScript中已廣泛使用(例如Ajax),服務端JavaScript的停滯給Ryan Dahl留出了巨大空白——于是Node.js誕生了。
Node.js的出現給服務端JavaScript帶來了新氣象,CommonJS也應用到了Node.js的模塊和包上。憑借V8和異步I/O帶來的性能優勢,一掃曾經的陰霾。尤其是將異步引入到業務層,非阻塞特性使得JavaScript的語言模型顯得足夠獨特。
Node.js 與Nginx之間也可以稍作比較。通過我們的線下測試,發現Nginx的性能比Node.js的HTTP模塊要好很多。但Nginx考量的是面向客戶端, 后端業務方面依然是受具體業務影響。而Node.js則可以利用異步I/O來實現業務并行,以提升效率。從這點看,Nginx沒有Node.js靈活。此 外,Node.js后來的發展方向不再單獨是一個Web服務器,而是一個面向網絡的平臺,它甚至可以是TCP服務器,或者變身為遠端服務器的客戶端。
異步的利與弊
異 步帶來的好處主要是性能方面,如果沒有異步,就沒有高性能服務器。異步讓CPU與I/O之間重疊利用,可以很好地利用硬件,提高吞吐率。異步早就應用在操 作系統層面中,在操作系統內部,處處是信號量,只是在業務層面出現較少。將Node.js廣泛應用異步到業務層,在面向網絡時的效果是明顯的。
為了直觀地展示在網絡中應用異步I/O的重要性,下面這組數據必不可少:

表2 網絡中的I/O開銷
我給出的題目很簡單:如果在分布式環境中,需要從不同的地方調用10次不同的數據,時間消耗為t1到t10不等,總計消耗時間該為多少?如果沒有異步I/O,同步方式將會花費很多時間,而理想狀況下的異步調用則為max(t1,…,t10)。
異 步令人不滿的地方在于流程控制會帶來麻煩。但以日常生活為例,生活中處處是異步:排隊到窗口買飯是同步,坐在桌子上點菜則是異步;網購是異步,自己跑去超 市買東西拎回家則是同步。我們可以發現生活中的異步并未給我們帶來多大煩惱,相反,同步的痛楚可能更明顯。但在代碼中,卻又是相反的。不過好在問題總是可 解的:將邏輯分發,或者采用趙劼開發的Wind.js都是可選方案。
擁抱開源與生態圈成型
隨 著Node.js帶來服務端JavaScript的進步,如今JavaScript的生態圈已經形成。這里有個有趣的現象,CommonJS立下的志愿最 初沒有被實現,但隨著Node.js采用CommonJS的部分規范,Node.js具備了理論上的指導。模塊、包規范的應用,使得Node.js不像最 初瀏覽器中的JavaScript一樣無序混亂。Node.js在應用過程中催生了NPM工具,而NPM工具的廣泛應用則起到了推廣CommonJS規范 的效果。這是一個種因得果的循環,有理想,則最終有其實現。
另一件類似的現象體現在V8與Node.js的關系上,Node.js因為V8的優異性能,選擇以它作為基礎構建平臺,但隨著Node.js不同于瀏覽器的應用場景,又反過來促進V8的性能提升。
Node.js 自身開源,同時也推動開源,NPM上的模塊數量如今已超過14000,日下載量接近50萬次,社區的活躍程度史無前例。盡管我們知道Node.js繼承了 一些JavaScript帶來的缺點,但在如此強大的社區面前,在一個解決方案比問題還多的社區面前,想沒有信心都難。
作為一家企業,我們在淘寶內部也搭建了NPM的鏡像服務器,充分利用NPM來管理內部私有模塊,減少拷貝粘貼行為,將一切可以模塊化的功能或方案盡皆做成包,共享給兄弟項目或者發布到官方NPM上,回饋給開源社區。
如 果一個開源社區的循環是良性的,我們沒有理由不信任它,因為成型生態圈,使它具有持久的生命力。但有一件事必須明了,盡管這個生態圈已然成型,但 Node.js自身與現有技術并不是非此即彼的,發揮異步并行的優勢,與其他系統異構共存,將會是很長一段時間內的主題。即便是理念最為相近的 Nginx,也是可以與Node.js互相協助的。
總結
也許是早產注定了JavaScript留給開發者發揮的余地夠多,盡管不是有意缺少,但確實體現出了Less is More的精髓。按趙劼的話說,盡管JavaScript有很多毛病,但它設計得還不賴。相信后續的ECMAScript標準化能夠修繕過去的問題。在經 歷了多年的發展以后,JavaScript已不是從前的它,從核心上衍生出的紅利也是顯而易見的,生態圈及前后端語言打通,使得更多開發者可以參與其中。 隨著Node.js在服務端大展拳腳,第二波JavaScript浪潮已經到來,如果依舊懷抱過去的觀點,等著被恭喜Out吧。
作者田永強,花名樸靈,2011年在上海組織CNode社區的線下分享會。2012年加入淘寶的數據產品團隊專職參與Node.js開發。

