科普文一則,說(shuō)說(shuō)我對(duì)Node.js的一些認(rèn)識(shí),以及我作為前端工程師為什么會(huì)向后端工程師推薦Node.js。 “Node.js 是服務(wù)器端的 JavaScript 運(yùn)行環(huán)境,它具有無(wú)阻塞(non-blocking)和事件驅(qū)動(dòng)(event-driven)等的特色,Node.js 采用V8引擎,同樣,Node.js實(shí)現(xiàn)了類似 Apache 和 nginx 的web服務(wù),讓你可以通過(guò)它來(lái)搭建基于 JavaScript的Web App�!� 我想不僅僅是Node.js,當(dāng)我們要引入任何一種新技術(shù)前都必須要搞清楚幾個(gè)問(wèn)題: 1. 我們遇到了什么問(wèn)題? 2. 這項(xiàng)新技術(shù)解決什么問(wèn)題,是否契合我們遇到的問(wèn)題? 3. 我們遇到問(wèn)題的多種解決方案中,當(dāng)前這項(xiàng)新技術(shù)的優(yōu)勢(shì)體現(xiàn)在哪兒? 4. 使用新技術(shù),帶來(lái)哪些新問(wèn)題,嚴(yán)重么,我們能否解決掉? 我們的問(wèn)題:Server端阻塞 Node.js被設(shè)計(jì)用來(lái)解決服務(wù)端阻塞問(wèn)題.下面通過(guò)一段簡(jiǎn)單的代碼解釋何為阻塞:
這段代碼的問(wèn)題是在上面兩個(gè)語(yǔ)句之間,在整個(gè)數(shù)據(jù)查詢的過(guò)程中,當(dāng)前程序進(jìn)程往往只是在等待結(jié)果的返回.這就造成了進(jìn)程的阻塞.對(duì)于高并發(fā),I/O 密集行的網(wǎng)絡(luò)應(yīng)用中,一方面進(jìn)程很長(zhǎng)時(shí)間處于等待狀態(tài),另一方面為了應(yīng)付新的請(qǐng)求不斷的增加新的進(jìn)程.這樣的浪費(fèi)會(huì)導(dǎo)致系統(tǒng)支持QPS遠(yuǎn)遠(yuǎn)小于后端數(shù)據(jù)服 務(wù)能夠支撐的QPS,成為了系統(tǒng)的瓶頸.而且這樣的系統(tǒng)也特別容易被慢鏈接攻擊(客戶端故意不接收或減緩接收數(shù)據(jù),加長(zhǎng)進(jìn)程等待時(shí)間)。 如何解決阻塞問(wèn)題 可以引入事件處理機(jī)制解決這個(gè)問(wèn)題。在查詢請(qǐng)求發(fā)起之前注冊(cè)數(shù)據(jù)加載事件的響應(yīng)函數(shù),請(qǐng)求發(fā)出之后立即將進(jìn)程交出,而當(dāng)數(shù)據(jù)返回后再觸發(fā)這個(gè)事件并在預(yù)定好的事件響應(yīng)函數(shù)中繼續(xù)處理數(shù)據(jù):
我們看到若按照這個(gè)思路解決阻塞問(wèn)題,首先我們要提供一套高效的異步事件調(diào)度機(jī)制.而主要用于處理瀏覽器端的各種交互事件的JavaScript。相對(duì)于其他語(yǔ)言,至少有兩個(gè)關(guān)鍵點(diǎn)特別適合完成這個(gè)任務(wù)。 為什么JS適合解決阻塞問(wèn)題 首先JavaScript是一種函數(shù)式編程語(yǔ)言,函數(shù)編程語(yǔ)言最重要的數(shù)學(xué)基礎(chǔ)是λ演算(lambda calculus) — 即函數(shù)對(duì)象可以作為其他函數(shù)對(duì)象的輸入(參數(shù))和輸出(返回值)。 這個(gè)特性使得為事件指定回調(diào)函數(shù)變得很容易。特別是JavaScript還支持匿名函數(shù)。通過(guò)匿名函數(shù)的輔助,之前的代碼可以進(jìn)行簡(jiǎn)寫如下:
還有另一個(gè)關(guān)鍵問(wèn)題是,異步回調(diào)的運(yùn)行上下文保持(本文暫稱其為”狀態(tài)保持”)。我們先來(lái)看一段代碼來(lái)說(shuō)明何為狀態(tài)保持:
前面的寫法在傳統(tǒng)的阻塞是編程中非常常見(jiàn),但接下來(lái)進(jìn)行異步改寫時(shí)會(huì)遇到一些困擾:
細(xì)心的朋友可能已經(jīng)注意到,當(dāng)?shù)却薾秒數(shù)據(jù)查詢結(jié)果返回后執(zhí)行回調(diào)時(shí)。回調(diào)函數(shù)中卻仍然使用了main函數(shù)的局部變量”id”,而”id”似乎應(yīng) 該在n秒前走出其作用域。為什么此時(shí)”id”仍然可以訪問(wèn)呢,這是因?yàn)镴avaScript的另外一個(gè)重要語(yǔ)言特性:閉包(Closures)。接下來(lái)我 來(lái)詳解閉包的原委。 在復(fù)雜的應(yīng)用中,我們一定會(huì)遇到這類場(chǎng)景。即在函數(shù)運(yùn)行時(shí)需要訪問(wèn)函數(shù)定義時(shí)的上下文數(shù)據(jù)(注意:一定要區(qū)分函數(shù)定義時(shí)和函數(shù)運(yùn)行時(shí)兩個(gè)不同的時(shí) 刻)。特別是在異步編程模型中,函數(shù)的定義和運(yùn)行又分處不同的時(shí)間段,那么保持上下文的問(wèn)題變得更加突出了。因?yàn)槲覀冊(cè)谌蝿?wù)執(zhí)行一半時(shí)把資源交出去沒(méi)有問(wèn) 題,但當(dāng)任務(wù)需要再次繼續(xù)時(shí)我們必須還原現(xiàn)場(chǎng)。 在這個(gè)例子中,db.query作為一個(gè)公共的數(shù)據(jù)庫(kù)查詢方法,把”id”這個(gè)業(yè)務(wù)數(shù)據(jù)傳入給db.query,交由其保存是不太合適的。但我們可 以稍作抽象,讓db.query再支持一個(gè)需要保持狀態(tài)的數(shù)據(jù)對(duì)象傳入,當(dāng)數(shù)據(jù)查詢完畢后可以把這些狀態(tài)數(shù)據(jù)原封不動(dòng)的回傳。如下:
記住這種重要的思路,我們?cè)倏纯词欠襁能進(jìn)一步的抽象?可以的,不過(guò)接下的動(dòng)作之前,我們還要了解在JavaScript中一個(gè)函數(shù)也是一個(gè)對(duì)象。 一個(gè)函數(shù)實(shí)例fn除了函數(shù)體的定義之外,我們?nèi)匀豢梢栽谶@個(gè)函數(shù)對(duì)象實(shí)例之本身擴(kuò)展其他屬性,如fn.a=1;受到這個(gè)啟發(fā)我們嘗試把需要保持的狀態(tài)直接 綁定到函數(shù)實(shí)例上:
我們做了什么?生成了currentState對(duì)象,然后在函數(shù)onDataLoad定義時(shí),將currentState綁定給 onDataLoad這個(gè)函數(shù)實(shí)例。那么在onDataLoad運(yùn)行時(shí),就可以拿到定義時(shí)的state對(duì)象了。JavaScript的閉包特性就是內(nèi)置了 這個(gè)過(guò)程而已。 在每個(gè)JavaScript函數(shù)運(yùn)行時(shí),都有一個(gè)運(yùn)行時(shí)內(nèi)部對(duì)象稱為Execution Context,它包含如下Variable Object(VO,變量對(duì)象), Scope Chain(作用域鏈)和”this” Value三部分。如圖: ![]() 圖片來(lái)自ECMA-262 JavaScript .The Core 其中變量對(duì)象VO,包含了所有局部變量的引用。對(duì)于main函數(shù),局部變量”id”存儲(chǔ)在VO.id內(nèi)�?雌饋�(lái)用VO來(lái)代替我們的 currentSate最合適了。但main函數(shù)還可能嵌套在其他函數(shù)之內(nèi),所以我們需要ScopeChain,它是一個(gè)包含當(dāng)前運(yùn)行函數(shù)VO和其所有父 函數(shù)scope的數(shù)組。 所以在這個(gè)例子中,在onDataLoad函數(shù)定義時(shí),就為默認(rèn)為其綁定了一個(gè)[[scope]]屬性指向其父函數(shù)的 ExecutionContext的ScopeChain。而當(dāng)函數(shù)onDataLoad執(zhí)行時(shí),就可以通過(guò)[[scope]]屬性來(lái)訪問(wèn)父函數(shù)的VO對(duì) 象來(lái)找到id,如果父函數(shù)的VO中沒(méi)有id這個(gè)屬性,就再繼續(xù)向上查找其祖先的VO對(duì)象,直到找到id這個(gè)屬性或到達(dá)最外層返回undefined。也正 是因?yàn)檫@個(gè)引用,造成VO的引用計(jì)數(shù)不為0,在走出作用域時(shí),才不會(huì)被垃圾回收。 很多朋友覺(jué)得閉包較難理解,其實(shí)我們只要能明確的區(qū)分函數(shù)定義和函數(shù)運(yùn)行兩個(gè)時(shí)機(jī),那么閉包就是讓函數(shù)在運(yùn)行時(shí)能夠訪問(wèn)到函數(shù)定義時(shí)的所處作用域內(nèi)的所有變量,或者說(shuō)函數(shù)定義時(shí)能訪問(wèn)到什么變量,那么在函數(shù)運(yùn)行時(shí)通過(guò)相同的變量名一樣能訪問(wèn)到。 關(guān)于狀態(tài)保持是本文的重點(diǎn),在我看到的多數(shù)Node.js的介紹文章中并沒(méi)有詳解這里,我們只是知道了要解決阻塞問(wèn)題,但是JavaScript解決阻塞問(wèn)題的優(yōu)勢(shì)到底在哪里,作為一名前端工程師,我想有必要花一些篇幅詳細(xì)解釋一下。 而之所以我叫它”狀態(tài)保持”因?yàn)檫有一個(gè)非常相似的場(chǎng)景可以類比: 用戶從A頁(yè)面提交表單到B頁(yè)面,如果提交數(shù)據(jù)校驗(yàn)不通過(guò),則需要返回A頁(yè)面,同時(shí)保持用戶在A頁(yè)面填寫的內(nèi)容并提示用戶修改不對(duì)的地方。從提交到校驗(yàn)出錯(cuò)再返回繼續(xù)填寫是一個(gè)包含網(wǎng)絡(luò)交互的異步過(guò)程,這相當(dāng)于填寫表單這個(gè)任務(wù)過(guò)會(huì)兒再繼續(xù)。 在傳統(tǒng)網(wǎng)頁(yè)開(kāi)發(fā)中,用戶的狀態(tài)通過(guò)請(qǐng)求傳遞到服務(wù)端,交由后端狀態(tài)保持(類似交給db.query的currentSate)。而使用Ajax的網(wǎng) 頁(yè),因?yàn)椴⑽措x開(kāi)原頁(yè)面,那么服務(wù)端只要負(fù)責(zé)校驗(yàn)用戶提交的數(shù)據(jù)是否正確即可,發(fā)送錯(cuò)誤,返回錯(cuò)誤處相關(guān)信息即可,這就是所謂前端狀態(tài)保持�?梢钥吹竭@個(gè) 場(chǎng)景里邊服務(wù)端做的事情變少了,變純粹了。正如我們的例子中db.query不再存儲(chǔ)轉(zhuǎn)發(fā)第三個(gè)state參數(shù),變得更在輕量。 我們看到通過(guò)JavaScript函數(shù)式語(yǔ)言特性,匿名函數(shù)支持和閉包很漂亮的解決了同步編程到異步編程轉(zhuǎn)化過(guò)程中遇到的一系列最重要的問(wèn)題。但JavaScript是否就是最好的?這就要回答我們引用新技術(shù)時(shí)需要考慮的最后一個(gè)問(wèn)題了。 使用Node.js是否帶來(lái)額外的困擾,如何解決? Node.js性能真的是最好么?不用比較我們也可以得到結(jié)論,Node.js做無(wú)阻塞編程性能較難做到極致。何為極致?處理一個(gè)請(qǐng)求需要占用多少 內(nèi)存,多少cpu資源,多少帶寬,有丁點(diǎn)浪費(fèi)就不是極致。阻塞式編程浪費(fèi)了大量進(jìn)程資源只是在等待,導(dǎo)致大量?jī)?nèi)存和cpu的浪費(fèi)。在這方面Node.js 好很多,但也正是因?yàn)橐恍╅]包等JavaScript內(nèi)建機(jī)制也會(huì)導(dǎo)致資源的浪費(fèi),看下面的代碼:
至少整個(gè)數(shù)據(jù)查詢過(guò)程中,變量str所使用的2M內(nèi)存并不會(huì)被釋放,而str保持下去可能并沒(méi)有意義。前面已經(jīng)解釋過(guò)閉包的原理,閉包并沒(méi)有智能到只包起來(lái)今后可能被訪問(wèn)到的對(duì)象。即使不了解閉包的原理,也可以通過(guò)一段簡(jiǎn)單腳本驗(yàn)證這點(diǎn):
我們?cè)诨卣{(diào)函數(shù)當(dāng)中只設(shè)置一個(gè)斷點(diǎn),并不指明我們要訪問(wèn)哪個(gè)變量。然后我們?cè)诳刂婆_(tái)監(jiān)視一下,id和str都是可以拿到的。 所以我來(lái)猜想一下,性能極端苛刻的場(chǎng)景,無(wú)阻塞是未來(lái),但無(wú)阻塞發(fā)展下去,或者有更輕量的腳本引擎產(chǎn)生,或者JavaScript引擎可能要調(diào)整可以disable閉包,或者我們要通過(guò)給JS開(kāi)發(fā)靜態(tài)編譯器在代碼發(fā)布前自動(dòng)優(yōu)化我們的代碼。 靜態(tài)編譯是如今JavaScript技術(shù)領(lǐng)域的又一個(gè)熱點(diǎn),我們都知道JavaScript是解釋型腳本語(yǔ)言,在運(yùn)行時(shí)自動(dòng)編譯。但是運(yùn)行時(shí)編譯只是將代碼轉(zhuǎn)為機(jī)器碼執(zhí)行,卻并未覆蓋傳統(tǒng)編譯型語(yǔ)言在編譯階段所做的任務(wù)。比如,語(yǔ)法檢查,接口校驗(yàn),全局性能優(yōu)化等等。 最常見(jiàn)的JavaScript靜態(tài)編譯就是腳本壓縮工具,在代碼發(fā)布到線上之前,我們通過(guò)各種壓縮工具,將代碼壓縮,達(dá)到減少網(wǎng)絡(luò)傳輸量的問(wèn)題。而 在這個(gè)時(shí)間點(diǎn),已經(jīng)有越來(lái)越多的事情可做,比如:Google利用ClouserComplier提供的系列編譯指令,讓JavaScript更好的實(shí)現(xiàn) OO編程。也有GWT,CoffeeScript這樣的項(xiàng)目,將其他語(yǔ)言編譯為JavaScript。在淘寶我們?cè)诖a靜態(tài)編譯階段來(lái)解決因 JavaScript細(xì)粒度模塊化改造引入各種性能問(wèn)題,也用來(lái)對(duì)第三方提供JavaScript代碼進(jìn)行一定的安全檢查。 我們期待前面的代碼經(jīng)過(guò)靜態(tài)編譯器編譯后變成如下結(jié)果:
除了性能方面的擔(dān)憂,使用Node.js進(jìn)行編程增加了代碼編寫的復(fù)雜度。因?yàn)槲覀兞?xí)慣于阻塞式編程的寫法,切換到異步模式編程,往往對(duì)于太多多層次的callback函數(shù)嵌套弄得不知所措。老趙最近開(kāi)發(fā)了項(xiàng)目JSCEX正是要解決這個(gè)問(wèn)題,它讓大家在遵守一些小的約定后,能夠仍然保持同步編程的寫法進(jìn)行代碼開(kāi)發(fā)。寫完的代碼同樣通過(guò)靜態(tài)編譯器編譯成異步回調(diào)式模式的代碼再交給JavaScript引擎執(zhí)行。 Node.js還要解決什么問(wèn)題 說(shuō)了這么多,無(wú)阻塞編程要做的還遠(yuǎn)不止這些。首先需要一個(gè)高效的JS引擎,高效的事件池和線程池。另外幾乎所有和Node.js交互的傳統(tǒng)模塊如文件系統(tǒng),數(shù)據(jù)訪問(wèn),HTTP解析,DNS解析都是阻塞式的,都需要額外改造。 Node.js作者極其團(tuán)隊(duì),正是認(rèn)清問(wèn)題所在以及JS解決這些問(wèn)題方面的優(yōu)勢(shì)�;贕oogle開(kāi)源的高效JavaScript引擎V8,貢獻(xiàn)了大量的智慧和精力解決上述大部分問(wèn)題后才有Node.js橫空出世。 當(dāng)前Node社區(qū)如此火熱,千余開(kāi)源的Node.js模塊,活躍在WebFramework,WebSocket,RPC,模板引擎,數(shù)據(jù)抓取服務(wù),圖形圖像幾乎所有工程領(lǐng)域。 后記 本文主要的信息來(lái)自Node.js作者在JSConf09,JSConf10上的分享。 而作為前端開(kāi)發(fā),著重講了函數(shù)式編程,閉包對(duì)于無(wú)阻塞開(kāi)發(fā)的重要意義。我期待這篇文章能夠給前端和后端工程師都帶來(lái)收獲。 同樣作為前端開(kāi)發(fā),不得不再插幾句,說(shuō)說(shuō)服務(wù)端JS能夠解決的另一個(gè)問(wèn)題:當(dāng)前的Web開(kāi)發(fā)前后端使用不同的語(yǔ)言,很多相同的業(yè)務(wù)邏輯要前后端分別 用不同語(yǔ)言重復(fù)實(shí)現(xiàn)。比如越來(lái)越多重度依賴JavaScript的胖客戶端應(yīng)用,當(dāng)客戶瀏覽器禁用JavaScript時(shí),則需要使用服務(wù)端語(yǔ)言將主業(yè)務(wù) 流程再實(shí)現(xiàn)一次,這即是前端常說(shuō)的”漸進(jìn)增強(qiáng)”。 當(dāng)我們擁有了服務(wù)端JavaScript語(yǔ)言,我們自然就會(huì)想到能否利用Node.js做到”一次開(kāi)發(fā),漸進(jìn)增強(qiáng)”。解決掉這個(gè)為小量用戶,浪費(fèi)大量時(shí)間的惱人的問(wèn)題。這方面的實(shí)踐,YAHOO仍然是先驅(qū),早在一年多前開(kāi)始YAHOO通過(guò)nodejs-yui3項(xiàng)目做了很多卓越的貢獻(xiàn),而淘寶自主開(kāi)發(fā)的前端框架Kissy也有服務(wù)端運(yùn)行的相關(guān)嘗試。 JavaScript在誕生之時(shí)就不僅僅是瀏覽器端工具,如今JavaScript能夠再一次回到服務(wù)端展示拳腳,感謝V8,感謝NodeJS作者,團(tuán)隊(duì)和社區(qū)的諸多貢獻(xiàn)者,祝Node好運(yùn),JavaScript好運(yùn)。 關(guān)于作者 李穆,前端工程師,就職于淘寶廣告技術(shù)部架構(gòu)組,淘寶花名:李牧,專注淘寶廣告引擎和業(yè)務(wù)系統(tǒng)前端開(kāi)發(fā)。 |