1. 程式人生 > >Jscex專案現狀:UglifyJS解析器及AOT編譯器

Jscex專案現狀:UglifyJS解析器及AOT編譯器

先來一段廣告:第四屆nBazaar技術交流會將於2011年4月23日舉行。第四屆交流會的形式將略作改變:除了三場演講(Windows Phone 7、IDE外掛開發、單點登陸解決方案設計與實現)之外,本次活動設有嘉賓互動環節,您將有機會和嘉賓就某些話題進行探討。我們正在收集話題,也希望大家踴躍提問,具體資訊詳見http://nbazaar.org/。此外,nBazaar技術沙龍的郵件列表已經正式啟用,所有使用者也已新增完成(之前報名或參加過技術會議)。如果有任何疑問,請郵件至

Jscex專案是我為了簡化JavaScript非同步的一個類庫,支援任意JavaScript(ECMASCript 3)引擎。Jscex小巧而強大,可以極大地改善前端的AJAX及動畫等場景的程式設計體驗,同樣也可以用在

node.js進行伺服器開發。從產生Jscex的想法到現在也有幾個月的時間了,也一直想設法進行推廣。在思考過程也發現了它在實際生產中可能會遇到的問題,於是前兩個星期的主要工作,便是針對這些問題進行優化。首先我將Jscex的JavaScript分析器從Narcissus換成了UglifyJS,並基於node.js開發了一個簡單的AOT編譯器。接下來我也打算寫個稍微詳細一點的介紹,然後在國外社群看看反響如何。

Jscex的本質是一個用JavaScript編寫的JavaScript編譯器,因此我需要一個JavaScript實現的JavaScript解析器。我起初選擇了著名的Narcissus專案,但由於它用到了

SpiderMonkey的一些擴充套件,最終我使用的其實是NarrativeJS中舊版的Narcissus程式碼。我一直在設法減小Jscex核心的體積及執行速度(畢竟一個重要的場景是瀏覽器端),再加上不是很喜歡舊版Narcissus程式碼的解析結果,於是我也在不斷尋找它的替代品。前段時間我發現了UglifyJS這個JavaScript壓縮器,它的解析器移植於parse-js專案,後者是一個用Common Lisp實現的類庫,因此輸出結構也十分簡單,一個“表”而已,執行速度也大大領先於Narcissus,體積也更小。於是我花了一個週末的時間將Jscex編譯器改寫為基於UglifyJS的實現。

在改寫過程中,我也同樣考慮了目的碼在壓縮後的體積。我使用

Closure Compiler的“高階”模式壓縮程式碼,一般來說Closure Compiler的高階模式很破壞程式碼,我使用了各種方式來保證壓縮後的程式碼能夠正確執行。目前,如果您要在專案中使用Jscex編寫非同步程式,需要依次載入以下三個檔案(它們都在專案原始碼的bin目錄中):

  • 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專案,如果遇到問題請及時與我聯絡,我會給予您必要的支援。