科普文一則,說說我對NodeJS(一種服務端JavaScript實現)的一些認識,以及我爲何會向後端工程師推薦NodeJS.
"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被設計用來解決服務端阻塞問題.經過一段簡單的代碼解釋何爲阻塞:
javascript
這段代碼的問題是在上面兩個語句之間,在整個數據查詢的過程當中,當前程序進程每每只是在等待結果的返回.這就形成了進程的阻塞.對於高併發,I/O密集行的網絡應用中,一方面進程很長時間處於等待狀態,一方面爲了應付新的請求不斷的增長新的進程.這樣的浪費會致使系統支持QPS遠遠小於後端數據服務可以支撐的QPS,成爲系統的瓶頸.並且這樣的系統也特別容易被慢連接攻擊(客戶端故意不接收或減緩接收數據,加長進程等待時間).
如何解決阻塞問題
解決這個問題的辦法是,創建一種事件機制,發起查詢請求以後,當即將進程交出,當數據返回後觸發事件,再繼續處理數據:
前端
咱們看到按照這個思路解決阻塞問題,首先咱們要提供一套高效的異步事件調度機制.而主要用於處理瀏覽器端的各類交互事件的JavaScript.相對於其餘語言,至少有兩個關鍵點特別適合完成這個任務.
爲何JS適合解決阻塞問題
首先JavaScript是一種函數式編程語言,函數編程語言最重要的數學基礎是λ演算(lambda calculus) -- 即函數能夠接受函數看成輸入(參數)和輸出(返回值).
函數能夠做爲其餘函數的參數輸入的這個特性,使得爲事件指定回調函數變得很容易.特別是JavaScript還支持匿名函數.經過匿名函數的輔助,以前的代碼能夠進行簡寫以下.
java
還有一個關鍵問題是,異步回調的運行上下文保持(稱狀態保持),我看一段代碼來講明何爲狀態保持.
node
前面的寫法在傳統的阻塞是編程中很是常見,但接下來進行異步改寫時會遇到一些困擾.
nginx
細心的同窗能夠注意到,當等待了n秒數據查詢結果返回後執行回調時.回調函數中卻仍然使用了main函數的局部變量"id",而"id"已經在n秒前走出了其做用域,這是爲何呢?熟悉JavaScript的同窗會淡然告訴您:"這是閉包(closures)~".
其實在複雜的應用中,咱們必定會遇到這類場景.即在函數運行時須要訪問函數定義時的上下文數據(注意:必定要區分函數定義時和函數運行時這樣的字眼和其表明的意義,否則很快就會糊塗).而在異步編程中,函數的定義和運行又分處不一樣的時間段,那麼保持上下文的問題變得更加突出了.
在這個例子中,db.query做爲一個公共的數據庫查詢方法,把"id"這個業務數據傳入給db.query,交由其保存是不太合適的.但聰明的同窗們能夠抽象一下,讓db.query再支持一個須要保持狀態的數據對象傳入,當數據查詢完畢後能夠把這些狀態數據原封不動的回傳.以下:
git
記住這種重要的思路,咱們再看看是否還能進一步的抽象?能夠的,不過接下的動做以前,咱們先要了解在JavaScript中一個函數也是一個對象.一個函數實例fn除了具有可函數體的定義以外,仍然能夠在這個函數對象實例之上擴展屬性,如fn.a=1;受到這個啓發咱們嘗試把須要保持的狀態直接綁定到函數實例上.
github
咱們作了什麼?生成了currentState對象,而後在函數onDataLoad定義時,將currentState綁定給onDataLoad這個函數實例.那麼在onDataLoad運行時,就能夠拿到定義時的state對象了.而閉包就是內置了這個過程而已.
在每一個函數運行時,都有一個運行時對象稱爲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內建機制也會致使資源的浪費,看下面的代碼
web
直到數據查詢完成,變量str所使用的2M內存不會被釋放,而str保持下去可能並無意義.前面已經解釋過閉包的原理,閉包並無智能到只包起來從此可能被訪問到的對象.即便不瞭解閉包的原理,也能夠經過一段簡單腳本驗證這點:
數據庫
咱們在回調函數當中只設置一個斷點,並不指明咱們要訪問哪一個變量.而後咱們在控制檯監視一下,id和str都是能夠拿到的.(此處結論不嚴謹,各類新瀏覽器已經就此作了相關優化,詳見評論2樓,特別是2樓給出的詳細測試報告的鏈接)
因此我來不負責任的預測一下,性能極端苛刻的場景,無阻塞是將來,但無阻塞發展下去,或者有更輕量的腳本引擎產生(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好運.apache
原文:http://limu.iteye.com/blog/1013223