Jscex專案現狀:UglifyJS解析器及AOT編譯器
先來一段廣告:第四屆nBazaar技術交流會將於2011年4月23日舉行。第四屆交流會的形式將略作改變:除了三場演講(Windows Phone 7、IDE外掛開發、單點登陸解決方案設計與實現)之外,本次活動設有嘉賓互動環節,您將有機會和嘉賓就某些話題進行探討。我們正在收集話題,也希望大家踴躍提問,具體資訊詳見http://nbazaar.org/。此外,nBazaar技術沙龍的郵件列表已經正式啟用,所有使用者也已新增完成(之前報名或參加過技術會議)。如果有任何疑問,請郵件至。
Jscex專案是我為了簡化JavaScript非同步的一個類庫,支援任意JavaScript(ECMASCript 3)引擎。Jscex小巧而強大,可以極大地改善前端的AJAX及動畫等場景的程式設計體驗,同樣也可以用在
Jscex的本質是一個用JavaScript編寫的JavaScript編譯器,因此我需要一個JavaScript實現的JavaScript解析器。我起初選擇了著名的Narcissus專案,但由於它用到了
在改寫過程中,我也同樣考慮了目的碼在壓縮後的體積。我使用
- uglifyjs-parser.min.js:UglifyJS解析器,大小20K,gzip後8K。
- jscex.min.js:Jscex核心編譯器,大小5.5K,gzip後1.8K。
- jscex.async.min.js:Jscex非同步核心類庫,大小2K,gzip後0.9K。
如果您覺得gzip後10K左右的體積還是有些大,那麼也可以使用目前已經提供的AOT編譯器——雖然AOT編譯器的原始目的並不是為了減小體積。
Jscex改善非同步程式設計的原理,在於讓程式設計師直接編寫程式碼,使用普通的程式設計思路來實現演算法,包括是用try...catch來捕獲異常等等,而不會因為非同步所需要的回撥將程式碼拆得支離破碎。例如我們要實現氣泡排序演算法的動畫演示,也只需要使用傳統編碼方式實現演算法即可:
// 標準演算法 var bubbleSort = function (array) { for (var x = 0; x < array.length; x++) { for (var y = 0; y < array.length - x; y++) { if (array[y] > array[y + 1]) { swap(array, y, y + 1); } } } } // 演示動畫 var bubbleSortAsync = eval(Jscex.compile("async", function (array) { for (var x = 0; x < array.length; x++) { for (var y = 0; y < array.length - x; y++) { var r = $await(compareAsync(array[y], array[y + 1])); if (r > 0) { $await(swapAsync(array, y, y + 1)); } } } }));
Jscex.compile會解析程式碼,並生成非同步程式碼,並交給eval來解釋執行。bubbleSortAsync和其中呼叫的compareAsync(比較兩個元素大小,並暫停10毫秒)和swapAsync(交換兩個元素,繪圖,並暫停20毫秒)都是非同步方法。但是無論在編寫和使用上,非同步方法和同步演算法幾乎沒有區別——唯一的區別便是$await語句必須單起一行。這個限制一是為了保證開發人員可以明確分清普通的JavaScript程式碼及非同步方法呼叫,二便是為了簡化編譯器的實現。例如,“理想情況”下類似以下的程式碼也需要支援:
f(g(1), $await(...))
if (x > y && $await(...)) { ... }
尤其是第二行程式碼,$await可能由於短路而根本不會執行。為此,Jscex要求開發人員明確編寫這樣的程式碼:
var a1 = g(1); var a2 = $await(...); f(a1, a2); if (x > y) { var flag = $await(...); if (flag) { ... } }
我並不擔心這會讓開發人員編寫程式碼時有所不便,事實上F#的Async Workflow是有這般要求,我甚至敢保證未來C#的非同步特性也是類似的設計。但是,JavaScript有個重要的特點:它在實際使用時往往會被壓縮。如果僅僅是去除空白字元,那麼Jscex自然還可以正常工作。但事實上現代的JavaScript壓縮工具都會分析程式碼的語義,並重新生成體積更小的程式碼。例如之前的bubbleSortAsync經過壓縮便會成為:
var bubbleSortAsync=eval(Jscex.compile("async",function(a){for(var b=0;b<a.length;b++)for(var c=0;c<a.length-b;c++){var d=$await(compareAsync(a[c],a[c+1]));d>0&&$await(swapAsync(a,c,c+1))}}))
試看d>0&&$wait(...)這段程式碼,完全就讓Jscex無法工作了。為此,我為Jscex開發了AOT編譯器(scripts目錄下的jscexc.js及JscexExtractor.js檔案),即在部署前便對程式碼進行編譯並生成目的碼(之前是在執行時生成程式碼,即JIT編譯)。AOT編譯器同樣使用JavaScript編寫,使用node.js執行,這樣便可以直接使用Jscex的編譯器實現。與編譯器核心不同,AOT編譯器使用了最新版的Narcissus來解析程式碼,這是因為Narcissus能夠提供更豐富的解析結果,我可以直接獲得整個目標方法的起始和結束地址(不過有bug,我使用時繞開了),自然還包括原始程式碼,用起來十分方便。至於之前提到的依賴於SpiderMonkey擴充套件,體積較大,執行速度慢等缺點,對於AOT編譯器來說便完全不是問題了。
Jscex的AOT編譯器使用起來十分簡單:
node jscexc.js --input input_file --output output_file
例如,如果一個檔案包含之前的bubbleSortAsync方法,那麼經過AOT編譯器之後,它的程式碼便會被替換成為:
var bubbleSortAsync = (function (array) { var $_builder_$ = Jscex.builders["async"]; return $_builder_$.Start(this, function () { return $_builder_$.Delay(function () { var x = 0; return $_builder_$.Loop( function () { return x < array.length; }, function () { x++; }, $_builder_$.Delay(function () { return $_builder_$.Delay(function () { var y = 0; return $_builder_$.Loop( function () { return y < (array.length - x); }, function () { y++; }, $_builder_$.Delay(function () { return $_builder_$.Bind(compareAsync(...), function (r) { return $_builder_$.Delay(function () { if (r > 0) { return $_builder_$.Bind(swapAsync(...), function () { return $_builder_$.Normal(); }); } else { return $_builder_$.Normal(); } }); }); }), false ); }); }), false ); }); }); })
再進行壓縮,便不會產生任何問題了。從表面看起來,編譯後的Jscex程式碼體積大了不少,但是其中大部分為重複架子程式碼,壓縮比例一般也會比較大。使用AOT編譯後的程式碼有以下幾個好處:
- 經過JavaScript壓縮器處理後也能正確執行。
- 執行時只需要載入一個極小的jscex.async.min.js檔案(非同步核心類庫),gzip後大小不到1K。
- 由於程式碼在釋出前生成,節省了JIT編譯的開銷。
在我個人看來,目前的Jscex已經可以在一些比較正式的場合中使用了。Jscex功能強大,實現小巧,能夠與其它類庫同時使用(它只會在全域性物件上產生一個Jscex物件),接下來我也會為jQuery或MooTools等著名JavaScript框架/類庫提供Jscex的繫結。在此也希望您可以實際使用一下Jscex專案,如果遇到問題請及時與我聯絡,我會給予您必要的支援。