Jscex编译器更新:已支持嵌套Jscex函数
2011-04-30 00:11 by 老赵, 2458 visitsJscex的编译器更新了。之前的编译器并不会将一个Jscex函数内部的其他Jscex函数代码一并展开,这导致内嵌的Jscex函数会在外部函数调用时反复编译,性能开销较大;不过更重要问题,可能是AOT编译后的代码无法彻底解除与编译器的依赖。嵌套Jscex函数是否合理是一回事儿,使用者可以不去这么做,但是编译器本身还是该支持的。这也是Jscex编译器改进计划中的重要一步。
之前,如果您编写这样的函数:
var outerAsync = eval(Jscex.compile("async", function () { var innerAsync = eval(Jscex.compile("async", function () { // inner implementations })); }));
实际上编译器会生成这样的代码:
var outerAsync = (function () { var $$_builder_$$ = Jscex.builders["async"]; return $$_builder_$$.Start(this, $$_builder_$$.Delay(function () { var innerAsync = eval(Jscex.compile("async", function () { // inner implementations })); return $$_builder_$$.Normal(); }) ); })
这样在每次调用outerAsync的时候,都会重新启用Jscex.compile及eval的过程,这个性能开销还是比较可观的。不过目前的编译器已经会将内部的Jscex函数彻底展开:
var outerAsync = (function () { var $$_builder_$$_0 = Jscex.builders["async"]; return $$_builder_$$_0.Start(this, $$_builder_$$_0.Delay(function () { var innerAsync = (function () { var $$_builder_$$_1 = Jscex.builders["async"]; return $$_builder_$$_1.Start(this, // compiled inner implementations $$_builder_$$_1.Normal() ); }); return $$_builder_$$_0.Normal(); }) ); })
除了不会多次编译内部的Jscex函数外,还彻底解除了与Jscex编译器的依赖,AOT编译器表示情绪愉快。您可能发现了,原本的builder变量名是固定的,不过为了支持内嵌函数,我在代码里添加了一个种子,这样每次编译时的builder变量名就不同了。例如在上面的代码中,编译后的外层函数使用$$_builder_$$_0,而内层函数使用的则是$$_builder_$$_1。
于是现在我们便可以使用内嵌Jscex函数了,例如之前提到过的“动画”示例,现在便可以写为:
var moveSquareAsync = eval(Jscex.compile("async", function(e) { var moveAsync = eval(Jscex.compile("async", function(startPos, endPos, duration) { for (var t = 0; t < duration; t += 50) { e.style.left = (startPos.x + (endPos.x - startPos.x) * t / duration) + "px"; e.style.top = (startPos.y + (endPos.y - startPos.y) * t / duration) + "px"; $await(Jscex.Async.sleep(50)); } e.style.left = endPos.x; e.style.top = endPos.y; })); $await(moveAsync({x:100, y:100}, {x:400, y:100}, 1000)); $await(moveAsync({x:400, y:100}, {x:400, y:400}, 1000)); $await(moveAsync({x:400, y:400}, {x:100, y:400}, 1000)); $await(moveAsync({x:100, y:400}, {x:100, y:100}, 1000)); }));
内层的moveAsync函数直接使用外部函数的参数e,这代码绝对美观大方——至于是否真写这样的代码,就靠开发人员自身的考量了。
话说回来,其实让现有的编辑器支持内嵌的Jscex函数并不困难,事实上原本“不支持”的原因也只是“没想太多”,就这么机械地写下来了。例如最核心的Jscex.compile,其“逻辑”大约是这样的:
Jscex.compile = function (builderName, func) { var code = "var f = " + func.ToString() + ";"; var ast = UglifyJS.parse(code); // [ "toplevel", [ [ "var", [ [ "f", [...] ] ] ] ] ] var funcAst = ast[1][0][1][0][1]; // compile funcAst with builderName; }
简单地说,我是根据传入函数的AST生成代码,当然还有些中间过程,但总体而言的“输入”只是函数本身(外加个builderName)。而为了能够在“生成代码”的过程中继续编译内嵌函数,其实我们也只要支持“标准模式(即eval(Jscex.compile(...))这种形式)”的AST即可:
function compileStandardPattern(evalAst) { // ... } Jscex.compile = function (builderName, func) { var funcCode = func.toString(); var evalCode = "eval(Jscex.compile(" + JSON.stringify(builderName) + ", " + funcCode + "))" var evalCodeAst = UglifyJS.parse(evalCode); // [ "toplevel", [ [ "stat", [ "call", ... ] ] ] ] var evalAst = evalCodeAst[1][0][1]; var newCode = compileStandardPattern(evalAst); // ... }
compileStandardPattern方法会根据“标准模式”的AST输出展开后的代码。而对于从Jscex.compile调用中输入的函数对象,我们也将其构造为标准形式并生成AST。然后,compileStandardPattern方法内部在生成代码的时候,如果发现某一部分AST又恰好符合“标准模式”,则递归调用自身来生成内嵌Jscex函数的代码,最终便得到了充分展开的结果。
当然,这么做和AOT编译器都有一个限制:由于其展开方式依靠静态代码分析,因此只能识别出“标准模式”的代码。例如,以下代码可以在JIT编译器下正常工作:
var compile = Jscex.compile; var builderName = "async"; var func = function () { ... }; var newCode = compile(builderName, func); var funcAsync = eval(newCode);
但显然,如果这段代码出现在某个Jscex函数内部,或是使用AOT编译器,便无法将其展开了。不过这个“限制”只是理论上的,我还没有想到不用“标准模式”的理由。
看到 JsCex 的demo后非常有兴趣,不知道JsCex的性能怎么样,是否有性能上的数据指标可供参考呢?