我為什麼向後端工程師推薦Node.js

發表於2011-12-07

來源:李穆

科普文一則,說說我對Node.js的一些認識,以及我作為前端工程師為什麼會向後端工程師推薦Node.js。

“Node.js 是伺服器端的 JavaScript 執行環境,它具有無阻塞(non-blocking)和事件驅動(event-driven)等的特色,Node.js 採用V8引擎,同樣,Node.js實現了類似 Apache 和 nginx 的web服務,讓你可以通過它來搭建基於 JavaScript的Web App。”

我想不僅僅是Node.js,當我們要引入任何一種新技術前都必須要搞清楚幾個問題:

1. 我們遇到了什麼問題?

2. 這項新技術解決什麼問題,是否契合我們遇到的問題?

3. 我們遇到問題的多種解決方案中,當前這項新技術的優勢體現在哪兒?

4. 使用新技術,帶來哪些新問題,嚴重麼,我們能否解決掉?

我們的問題:Server端阻塞

Node.js被設計用來解決服務端阻塞問題.下面通過一段簡單的程式碼解釋何為阻塞:

這段程式碼的問題是在上面兩個語句之間,在整個資料查詢的過程中,當前程式程式往往只是在等待結果的返回.這就造成了程式的阻塞.對於高併發,I/O 密集行的網路應用中,一方面程式很長時間處於等待狀態,另一方面為了應付新的請求不斷的增加新的程式.這樣的浪費會導致系統支援QPS遠遠小於後端資料服 務能夠支撐的QPS,成為了系統的瓶頸.而且這樣的系統也特別容易被慢連結攻擊(客戶端故意不接收或減緩接收資料,加長程式等待時間)。

如何解決阻塞問題

可以引入事件處理機制解決這個問題。在查詢請求發起之前註冊資料載入事件的響應函式,請求發出之後立即將程式交出,而當資料返回後再觸發這個事件並在預定好的事件響應函式中繼續處理資料:

我們看到若按照這個思路解決阻塞問題,首先我們要提供一套高效的非同步事件排程機制.而主要用於處理瀏覽器端的各種互動事件的JavaScript。相對於其他語言,至少有兩個關鍵點特別適合完成這個任務。

為什麼JS適合解決阻塞問題

首先JavaScript是一種函數語言程式設計語言,函式程式語言最重要的數學基礎是λ演算(lambda calculus) — 即函式物件可以作為其他函式物件的輸入(引數)和輸出(返回值)。

這個特性使得為事件指定回撥函式變得很容易。特別是JavaScript還支援匿名函式。通過匿名函式的輔助,之前的程式碼可以進行簡寫如下:

還有另一個關鍵問題是,非同步回撥的執行上下文保持(本文暫稱其為”狀態保持”)。我們先來看一段程式碼來說明何為狀態保持:

前面的寫法在傳統的阻塞是程式設計中非常常見,但接下來進行非同步改寫時會遇到一些困擾:

細心的朋友可能已經注意到,當等待了n秒資料查詢結果返回後執行回撥時。回撥函式中卻仍然使用了main函式的區域性變數”id”,而”id”似乎應 該在n秒前走出其作用域。為什麼此時”id”仍然可以訪問呢,這是因為JavaScript的另外一個重要語言特性:閉包(Closures)。接下來我 來詳解閉包的原委。

在複雜的應用中,我們一定會遇到這類場景。即在函式執行時需要訪問函式定義時的上下文資料(注意:一定要區分函式定義時和函式執行時兩個不同的時 刻)。特別是在非同步程式設計模型中,函式的定義和執行又分處不同的時間段,那麼保持上下文的問題變得更加突出了。因為我們在任務執行一半時把資源交出去沒有問 題,但當任務需要再次繼續時我們必須還原現場。

在這個例子中,db.query作為一個公共的資料庫查詢方法,把”id”這個業務資料傳入給db.query,交由其儲存是不太合適的。但我們可 以稍作抽象,讓db.query再支援一個需要保持狀態的資料物件傳入,當資料查詢完畢後可以把這些狀態資料原封不動的回傳。如下:

記住這種重要的思路,我們再看看是否還能進一步的抽象?可以的,不過接下的動作之前,我們還要了解在JavaScript中一個函式也是一個物件。 一個函式例項fn除了函式體的定義之外,我們仍然可以在這個函式物件例項之本身擴充套件其他屬性,如fn.a=1;受到這個啟發我們嘗試把需要保持的狀態直接 繫結到函式例項上:

我們做了什麼?生成了currentState物件,然後在函式onDataLoad定義時,將currentState繫結給 onDataLoad這個函式例項。那麼在onDataLoad執行時,就可以拿到定義時的state物件了。JavaScript的閉包特性就是內建了 這個過程而已。

在每個JavaScript函式執行時,都有一個執行時內部物件稱為Execution Context,它包含如下Variable Object(VO,變數物件), Scope Chain(作用域鏈)和”this” Value三部分。如圖:

我為什麼向後端工程師推薦Node.js

圖片來自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,在走出作用域時,才不會被垃圾回收。

很多朋友覺得閉包較難理解,其實我們只要能明確的區分函式定義和函式執行兩個時機,那麼閉包就是讓函式在執行時能夠訪問到函式定義時的所處作用域內的所有變數,或者說函式定義時能訪問到什麼變數,那麼在函式執行時通過相同的變數名一樣能訪問到。

關於狀態保持是本文的重點,在我看到的多數Node.js的介紹文章中並沒有詳解這裡,我們只是知道了要解決阻塞問題,但是JavaScript解決阻塞問題的優勢到底在哪裡,作為一名前端工程師,我想有必要花一些篇幅詳細解釋一下。

而之所以我叫它”狀態保持”因為還有一個非常相似的場景可以類比:

使用者從A頁面提交表單到B頁面,如果提交資料校驗不通過,則需要返回A頁面,同時保持使用者在A頁面填寫的內容並提示使用者修改不對的地方。從提交到校驗出錯再返回繼續填寫是一個包含網路互動的非同步過程,這相當於填寫表單這個任務過會兒再繼續。

在傳統網頁開發中,使用者的狀態通過請求傳遞到服務端,交由後端狀態保持(類似交給db.query的currentSate)。而使用Ajax的網 頁,因為並未離開原頁面,那麼服務端只要負責校驗使用者提交的資料是否正確即可,傳送錯誤,返回錯誤處相關資訊即可,這就是所謂前端狀態保持。可以看到這個 場景裡邊服務端做的事情變少了,變純粹了。正如我們的例子中db.query不再儲存轉發第三個state引數,變得更在輕量。

我們看到通過JavaScript函式式語言特性,匿名函式支援和閉包很漂亮的解決了同步程式設計到非同步程式設計轉化過程中遇到的一系列最重要的問題。但JavaScript是否就是最好的?這就要回答我們引用新技術時需要考慮的最後一個問題了。

使用Node.js是否帶來額外的困擾,如何解決?

Node.js效能真的是最好麼?不用比較我們也可以得到結論,Node.js做無阻塞程式設計效能較難做到極致。何為極致?處理一個請求需要佔用多少 記憶體,多少cpu資源,多少頻寬,有丁點浪費就不是極致。阻塞式程式設計浪費了大量程式資源只是在等待,導致大量記憶體和cpu的浪費。在這方面Node.js 好很多,但也正是因為一些閉包等JavaScript內建機制也會導致資源的浪費,看下面的程式碼:

至少整個資料查詢過程中,變數str所使用的2M記憶體並不會被釋放,而str保持下去可能並沒有意義。前面已經解釋過閉包的原理,閉包並沒有智慧到只包起來今後可能被訪問到的物件。即使不瞭解閉包的原理,也可以通過一段簡單指令碼驗證這點:

我們在回撥函式當中只設定一個斷點,並不指明我們要訪問哪個變數。然後我們在控制檯監視一下,id和str都是可以拿到的。

所以我來猜想一下,效能極端苛刻的場景,無阻塞是未來,但無阻塞發展下去,或者有更輕量的指令碼引擎產生,或者JavaScript引擎可能要調整可以disable閉包,或者我們要通過給JS開發靜態編譯器在程式碼釋出前自動優化我們的程式碼。

靜態編譯是如今JavaScript技術領域的又一個熱點,我們都知道JavaScript是解釋型指令碼語言,在執行時自動編譯。但是執行時編譯只是將程式碼轉為機器碼執行,卻並未覆蓋傳統編譯型語言在編譯階段所做的任務。比如,語法檢查,介面校驗,全域性效能優化等等。

最常見的JavaScript靜態編譯就是指令碼壓縮工具,在程式碼釋出到線上之前,我們通過各種壓縮工具,將程式碼壓縮,達到減少網路傳輸量的問題。而 在這個時間點,已經有越來越多的事情可做,比如:Google利用ClouserComplier提供的系列編譯指令,讓JavaScript更好的實現 OO程式設計。也有GWT,CoffeeScript這樣的專案,將其他語言編譯為JavaScript。在淘寶我們在程式碼靜態編譯階段來解決因 JavaScript細粒度模組化改造引入各種效能問題,也用來對第三方提供JavaScript程式碼進行一定的安全檢查。

我們期待前面的程式碼經過靜態編譯器編譯後變成如下結果:

除了效能方面的擔憂,使用Node.js進行程式設計增加了程式碼編寫的複雜度。因為我們習慣於阻塞式程式設計的寫法,切換到非同步模式程式設計,往往對於太多多層次的callback函式巢狀弄得不知所措。老趙最近開發了專案JSCEX正是要解決這個問題,它讓大家在遵守一些小的約定後,能夠仍然保持同步程式設計的寫法進行程式碼開發。寫完的程式碼同樣通過靜態編譯器編譯成非同步回撥式模式的程式碼再交給JavaScript引擎執行。

Node.js還要解決什麼問題

說了這麼多,無阻塞程式設計要做的還遠不止這些。首先需要一個高效的JS引擎,高效的事件池和執行緒池。另外幾乎所有和Node.js互動的傳統模組如檔案系統,資料訪問,HTTP解析,DNS解析都是阻塞式的,都需要額外改造。

Node.js作者極其團隊,正是認清問題所在以及JS解決這些問題方面的優勢。基於Google開源的高效JavaScript引擎V8,貢獻了大量的智慧和精力解決上述大部分問題後才有Node.js橫空出世。

當前Node社群如此火熱,千餘開源的Node.js模組,活躍在WebFramework,WebSocket,RPC,模板引擎,資料抓取服務,圖形影像幾乎所有工程領域。

後記

本文主要的資訊來自Node.js作者在JSConf09JSConf10上的分享。 而作為前端開發,著重講了函數語言程式設計,閉包對於無阻塞開發的重要意義。我期待這篇文章能夠給前端和後端工程師都帶來收穫。

同樣作為前端開發,不得不再插幾句,說說服務端JS能夠解決的另一個問題:當前的Web開發前後端使用不同的語言,很多相同的業務邏輯要前後端分別 用不同語言重複實現。比如越來越多重度依賴JavaScript的胖客戶端應用,當客戶瀏覽器禁用JavaScript時,則需要使用服務端語言將主業務 流程再實現一次,這即是前端常說的”漸進增強”。

當我們擁有了服務端JavaScript語言,我們自然就會想到能否利用Node.js做到”一次開發,漸進增強”。解決掉這個為小量使用者,浪費大量時間的惱人的問題。這方面的實踐,YAHOO仍然是先驅,早在一年多前開始YAHOO通過nodejs-yui3專案做了很多卓越的貢獻,而淘寶自主開發的前端框架Kissy也有服務端執行的相關嘗試。

JavaScript在誕生之時就不僅僅是瀏覽器端工具,如今JavaScript能夠再一次回到服務端展示拳腳,感謝V8,感謝NodeJS作者,團隊和社群的諸多貢獻者,祝Node好運,JavaScript好運。

關於作者

李穆,前端工程師,就職於淘寶廣告技術部架構組,淘寶花名:李牧,專注淘寶廣告引擎和業務系統前端開發。

相關文章