代码改变世界

Jscex项目现状:UglifyJS解析器及AOT编译器

2011-04-15 02:09 by Jeffrey Zhao, ... 阅读, ... 评论, 收藏, 编辑

先来一段广告:第四届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项目,如果遇到问题请及时与我联系,我会给予您必要的支持。

原文:《Jscex项目现状:UglifyJS解析器及AOT编译器

使用Live Messenger联系我