"Node.js 是服務器端的 JavaScript 運行環境,它具有無阻塞(non-blocking)和事件驅動(event-driven)等的特色,Node.js 採用 V8 引擎,同樣,Node.js 實現了類似Apache 和nginx 的web服務,讓你可以通過它來搭建基於 JavaScript 的 Web App。"
上週末參與了CNodeJS社區的第一次北京聚會,現場氣氛非常的好.而作爲一名前端開發,我在後面的討論環節講了下我對NodeJS的看法,主要回答的問題是"我爲什麼會向後端工程師推薦NodeJS".這其實是去年年底大團隊技術總結的話題之一,包含在我之前發過的PPT:團隊年終技術Review中.因爲之前沒有準備,當天倉促上陣,也不知道說清楚了沒,不如就在這裏再詳細展開記錄下.
我想不僅僅是NodeJS,當我們要引入任何一種新技術前都必須要搞清楚幾個問題:
1.我們遇到了什麼問題?
2.這項新技術解決什麼問題,是否契合我們遇到的問題?
3.我們遇到問題的多種解決方案中,當前這項新技術的優勢體現在哪兒?
4.使用新技術,帶來哪些新問題,嚴重麼,我們能否解決掉?
我們的問題:Server端阻塞
NodeJS被設計用來解決服務端阻塞問題.通過一段簡單的代碼解釋何爲阻塞:
- //根據ID,在數據庫中Persons表中查出Name
- var name = db.query("select name from persons where id=1");
- //進程等待數據查詢完畢,然後使用查詢結果.
- output("name")
如何解決阻塞問題
解決這個問題的辦法是,建立一種事件機制,發起查詢請求之後,立即將進程交出,當數據返回後觸發事件,再繼續處理數據:
- //定義如何後續數據處理函數
- function onDataLoad(name){
- output("name");
- }
- //發起數據請求,同時指定數據返回後的回調函數
- db.query("select name from persons where id=1",onDataLoad);
爲什麼JS適合解決阻塞問題
首先JavaScript是一種函數式編程語言,函數編程語言最重要的數學基礎是λ演算(lambda calculus) -- 即函數可以接受函數當作輸入(參數)和輸出(返回值).
函數可以作爲其他函數的參數輸入的這個特性,使得爲事件指定回調函數變得很容易.特別是JavaScript還支持匿名函數.通過匿名函數的輔助,之前的代碼可以進行簡寫如下.
- db.query("select name from persons where id=1",function(name){
- output(name);
- });
- //傳統同步寫法:將查詢和結果打印抽象爲一個方法
- function main(){
- var id = "1";
- var name = db.query("select name from persons where id=" + id);
- output("person id:" + id + ", name:" + name);
- }
- main();
- //異步寫法:
- function main(){
- var id = "1";
- db.query("select name from persons where id=" + id,function(name){
- output("person id:" + id + ", name:" + name);//n秒後數據返回後執行回調
- });
- }
- main();
其實在複雜的應用中,我們一定會遇到這類場景.即在函數運行時需要訪問函數定義時的上下文數據(注意:一定要區分函數定義時和函數運行時這樣的字眼和其代表的意義,不然很快就會糊塗).而在異步編程中,函數的定義和運行又分處不同的時間段,那麼保持上下文的問題變得更加突出了.
在這個例子中,db.query作爲一個公共的數據庫查詢方法,把"id"這個業務數據傳入給db.query,交由其保存是不太合適的.但聰明的同學們可以抽象一下,讓db.query再支持一個需要保持狀態的數據對象傳入,當數據查詢完畢後可以把這些狀態數據原封不動的回傳.如下:
- function main(){
- var id = "1";
- var currentState = new Object();
- currentState.person_id = id;
- db.query("select name from persons where id=" + id, function(name,state){
- output("person id:" + state.person_id + ", name:" + name);
- },currentState);//注意currentState是db.query的第三個參數
- }
- main();
- function main(){
- var id = "1";
- var currentState = new Object();
- currentState.person_id = id;
- function onDataLoad(name){
- output("person id:" + onDataLoad.state.person_id + ", name:" + name);
- }
- onDataLoad.state = currentState ;//爲函數指定state屬性,用於保持狀態
- db.query("select name from persons where id=" + id, onDataLoad);
- }
在每個函數運行時,都有一個運行時對象稱爲Execution context,它包含如下variable object(VO,變量對象),scope chain(作用域鏈)和thisValue三部分.詳見ECMA-262 JavaScript. The Core
其中變量對象VO,包含了所有局部變量的引用.對於main函數,局部變量"id"存儲在VO.id內.看起來用VO來代替我們的currentSate最合適了.但main函數還可能嵌套在其他函數之內,所以我們需要ScopeChain,它是一個包含當前運行函數VO和其所有父函數scope的數組.
所以在這個例子中,在onDataLoad函數定義時,就爲默認爲其綁定了一個[[scope]]屬性指向其父函數的ExecutionContext的ScopeChain.而當函數onDataLoad執行時,就可以通過[[scope]]屬性來訪問父函數的VO對象來找到id,如果父函數的VO中沒有id這個屬性,就再繼續向上查找其祖先的VO對象,直到找到id這個屬性或到達最外層返回undefined.也正是因爲這個引用,造成VO的引用計數不爲0,在走出作用域時,纔不會被垃圾回收.
很多人覺得閉包很難理解,其實我們只要能明確需要區分函數定義和函數運行這兩個時機,記住閉包讓函數在運行時能夠訪問到函數定義時的所處作用域內的所有變量.或者說函數定義時能訪問到什麼變量,那麼在函數運行時通過相同的變量名一樣能訪問到.
關於狀態保持是本文的重點,在我看到的多數NodeJS的介紹文章,並沒有詳解這裏,我們只是知道了要解決阻塞問題,但是JavaScript解決阻塞問題的優勢在哪裏,作爲一個前端開發,我想有必要詳細解釋一下.
其實說到狀態保持還有一個類似的場景,比如用戶從A頁面提交表單到B頁面,如果提交數據校驗不通過,則需要返回A頁面,同時保持用戶在A頁面填寫的內容並提示用戶修改不對的地方.從提交到返回顯示這也是一個包含網絡交互的異步過程.傳統網頁,用戶的狀態通過請求傳遞到服務端,交由後端狀態保持(類似交給db.query的currentSate).而使用Ajax的網頁,因爲並未離開原頁面,那麼服務端只要負責校驗用戶提交的數據是否正確即可,發送錯誤,返回錯誤處相關信息即可,這就是所謂前端狀態保持.可以看到這個場景裏邊服務端做的事情變少了,變純粹了.正如我們的例子中db.query不再存儲轉發第三個state參數,變得更輕量.
我們看到通過JavaScript函數式語言特性,匿名函數支持和閉包很漂亮的解決了同步編程到異步編程轉化過程中遇到的一系列最重要的問題.但JavaScript是否就是最好的?這就要回答我們引用新技術時需要考慮的最後一個問題了
使用NodeJS是否帶來額外的困擾,如何解決
性能真的是最好麼?不用比較我們也可以得到結論NodeJS,做無阻塞編程性能較難做到極致.何爲極致,處理一個請求需要佔用多少內存,多少cpu資源,多少帶寬,如果有浪費就不是極致.阻塞式編程浪費了大量進程資源只是在等待,導致大量內存和cpu的浪費.NodeJs好很多,但也正是因爲一些閉包等JS內建機制也會導致資源的浪費,看下面的代碼
- function main(){
- var id = "1";
- var str = "..."; //這裏存儲一個2M的字符串
- db.query("select name from persons where id=" + id,function(name){
- output("person id:" + id + ", name:" + name);//n秒後數據返回後執行回調
- });
- }
- main();
- function main(){
- var id = "1";
- var str = "..."; //這裏存儲一個2M的字符串
- window.setTimeout(function(){
- debugger; //我們在這裏設置斷點
- },10000)
- }
- main();
所以我來不負責任的預測一下,性能極端苛刻的場景,無阻塞是未來,但無阻塞發展下去,或者有更輕量的腳本引擎產生(lua?),或者V8JS引擎可能要調整可以disable閉包,或者我們可以通過給JS開發靜態編譯器在代碼發佈前優化我們的代碼.
我之前談到過JS靜態編譯器:"如果給JS代碼發佈正式使用前增加一個編譯步驟,我們能做些什麼",動態語言的實時編譯系統只完成了靜態語言編譯中的將代碼轉化爲字節碼的過程,而靜態語言編譯器的額外工作,如接口校驗,全局性能優化等待.所以JS也需要一個靜態的編譯器來完成這些功能,Google利用ClouserComplier提供了系列編譯指令,讓JS更好的實現OO編程,我來利用靜態編譯器解決一些JS做細粒度模塊化引入的性能方面的問題.而老趙最近的項目JSCEX,則也是利用JS發佈前的編譯環節重點解決異步編程的代碼複雜度問題.
我們習慣於阻塞式編程的寫法,切換到異步模式編程,往往對於太多多層次的callback嵌套弄得不知所措.所以老趙開發的JS靜態編譯器,借鑑F#的Computation Expressions,讓大家遵守一些小的約定後,能夠仍然保持同步編程的寫法,寫完的代碼通過JSCEX編譯爲異步回調式的代碼再交給JS引擎執行.
如果這個項目足夠好用,那就也解決了一個使用NodeJS這種新技術,卻加大編程複雜度這個額外引入的困擾.甚至可以沿着這個思路,在靜態編譯階段優化內存使用.
NodeJS還要解決什麼問題
說了這麼多,無阻塞編程要做的還遠不止這些.首先需要一個高效的JS引擎,高效的事件池和線程池.另外幾乎所有和NodeJS交互的傳統模塊如文件系統,數據訪問,HTTP解析,DNS解析都是阻塞式的,都需要額外改造.
正是NodeJS作者極其團隊,認清問題問題以及JS解決這些問題方面的優勢.基於高效的V8 JavaScript引擎,貢獻了大量的智慧和精力解決上述大部分問題後纔有NodeJS橫空出世.
當前Node社區如此火熱,千餘開源的NodeJS模塊,活躍在WebFramework,WebSocket,RPC,模板引擎,數據抓取服務,圖形圖像幾乎所有工程領域.
後記
本文主要的信息來自nodejs作者在JSConf09,JSConf10上的分享.
而作爲前端開發,着重講了函數式編程,閉包對於無阻塞開發的重要意義.我期待這篇文章能夠給前端和後端同學都帶來收穫.
同樣作爲前端開發,不得不再插幾句,說說服務端JS能夠解決的另一個問題:
當前的Web開發前後端使用不同的語言,很多相同的業務邏輯要前後端分別用不同語言重複實現.比如越來越多重度依賴JS的胖客戶端應用,當客戶瀏覽器禁用JavaScript時,則需要使用服務端語言將主業務流程再實現一次(這即是所謂的"漸進增強").
當我們擁有了服務端JavaScript語言,我們自然就會想到能否利用NodeJS做到"一次開發,漸進增強".解決掉這個爲小量用戶,浪費大量時間的惱人的問題.我們先要解決問題,這是使用NodeJS的最大動力.基於之前的統計,因爲各種原因瀏覽器不支持JS的用戶大概接近1%,至少淘寶絕對不會主動放棄這部分用戶.至於在服務端也使用JS是否能夠替掉LAMP架構,抑或NodeJS會對常見MVC架構帶來何種衝擊,V/C這些層是否能在前後端任意流動這些問題都是NodeJS解決問題後帶來的額外話題.
"一次開發,漸進增強"這方面的實踐,YAHOO仍然是先驅,早在一年多前開始YAHOO通過nodejs-yui3項目做了很多卓越的貢獻,而淘寶自主開發的前端框架Kissy也有服務端運行的相關嘗試,詳見我的同事拔赤的分享.而接下來的幾個月我也將在這方面做一些嘗試,有一定積累後我將再寫一篇文章更好的分析這個問題..
JS在誕生之時就不僅僅是瀏覽器端工具,如今JS能再一次回到服務端展示拳腳,感謝V8,感謝NodeJS作者,團隊和社區的諸多貢獻者,祝Node好運,JS好運.
評論
var Person = function(){};
Person.prototype = {
id : null,
name : null,
onload : function(name){
this.name = name;
console.log("person id:" + this.id + ",name:" + this.name);
}
};
function query(db,person){
db.query("select name from persons where id=" + person.id, function(thisRef){
return function(name){
thisRef.onload.call(thisRef, name);
};
}(person));
};
function main(){
var person = new Person();
person.id = "1";
query(db,person);
}
main();
看到你是想用this指向正在執行的函數,這個是不行的,可以理解函數是將一系列指定的動作,作用在一個對象之上,這個對象就是this,如果不指定,在瀏覽器環境裏this就是window。
想拿到正在執行的函數,也有辦法,通過arguments.callee,就拿到了。
詳見這篇文章專門討論這個問題的:http://otakustay.com/about-closure-and-gc/
似乎IE8及以前的IE,以及Opera的某些JS引擎版本,是跟我說的測試結果是一致的。
至於“閉包的外層變量爲什麼還存在是因爲閉包函數的scope還存有對其的引用。如果你沒有引用,他(遲早)是會被GC的”。似乎新瀏覽器對這個問題真正的優化方案是,如果引用了str,則保留,如果不引用str不保留(保留key值是null,或者壓根不保留),不存在先保留一會兒再回收的情況。這種優化方式,GC引用計數不再是記錄整個VO對象,而是細化拆分到VO的每個鍵。
function main(){
var id = "1";
var str = "..."; //這裏存儲一個2M的字符串
window.setTimeout(function(){
debugger; //我們在這裏設置斷點
},10000)
}
main();
此處中斷是無法拿到str的。不知道你怎麼測試的。可能是你真放了2M的字符串,或者是你的垃圾回收系統出BUG了。閉包的外層變量爲什麼還存在是因爲閉包函數的scope還存有對其的引用。如果你沒有引用,他(遲早)是會被GC的