项目

SNDA Code » Jscex

Jscex是JavaScript Computation EXpressions的缩写,它为JavaScript语言提供了一个monadic扩展,能够显著提高一些常见场景下的编程体验。Jscex项目完全使用JavaScript编写,能够在任意支持ECMAScript 3的执行引擎里使用,包括各浏览器及服务器端JavaScript环境(例如 Node.js )。

目前Jscex主要包括以下几点功能:

  • JIT编译器:在运行时动态编译代码,主要用于开发环境。
  • AOT编译器:在执行前静态编译代码。静态编译后的代码可以脱离JIT编译器执行,因此主要用于生产环境。
  • 异步编程库:基于Jscex生成的monadic代码,大大简化JavaScript下的异步编程难度。

Jscex异步编程库

异步编程的重要性不言而喻,对于JavaScript来说更是如此。JavaScript并没有提供任何能够阻塞代码执行过程的机制,任何一个“耗时”的操作都必须写成异步的模式。传统的异步操作会在完成时通过回调函数传回结果,我们可以在回调函数中进行下面的工作。

但这也是造成异步编程十分困难的主要原因。我们一直习惯于“线性”地编写逻辑,但是异步操作所带来的大量回调把我们的算法分解地支离破碎。我们不能用if来实现逻辑分支,也不能用while/for/do来实现循环。更不提异步操作之间的组合、错误处理以及取消操作了。

Jscex及它的异步编程库便是为了解决这些困难而诞生的。

使用方式

Jscex的核心功能是一个编译器,它可以将标准的JavaScript函数转化为monadic形式。编译器主要由三部分组成:

  • lib/json2.js: 由Douglas Crockfod编写的JSON 生成器,对于原生不支持JSON.stringify方法的环境(例如早些版本的IE),则需要引入该文件。
  • lib/uglifyjs-parser.js: UglifyJS 项目所使用的的JavaScript解析器,这是LISP项目 parse-js 的 JavaScript 移植。
  • src/jscex.js: JIT 编译器实现,负责在运行时生成代码。

以上三个文件都是未压缩的版本,适合在开发环境里使用。压缩后的版本在项目的bin文件夹中,适合在生产环境里使用(如果您并没有使用AOT编译器静态编译代码,请参考后续的的“AOT编译器”一节)。此外,开发版本的Jscex编译器会生成程序员友好的代码,对调试有所帮助,而压缩后的版本则会有略高一些的执行效率。

如果要使用Jscex的异步编程库,我们需要加载src/jscex.async.js文件。万事具备,我们便可以使用异步构造器来编译一个普通的JavaScript函数了:

1 var somethingAsync = eval(Jscex.compile("async", function (a, b) {
2     // implementation
3 }));

更详细的内容请参考后续章节。

示例

下面的所有示例都存放在samples/async目录中。

时钟

我们先来使用HTML 5里的canvas元素绘制一个时钟(samples/async/clock.html )。这对前端开发人员来说实在不是件难事:

1 function drawClock(time) {
2     // clear and canvas and draw a clock on it.
3 }
4 
5 setInterval(1000, function () {
6     drawClock(new Date());
7 });

这是使用回调函数的版本,不过在Jscex中代码是这样写的:

 1 var drawClockAsync = eval(Jscex.compile("async", function (interval) {
 2     while (true) {
 3         drawClock(new Date());
 4         // wait for an async operation to complete
 5         $await(Jscex.Async.sleep(interval));
 6     }
 7 }));
 8 
 9 drawClockAsync(1000).start();

我们在drawClockAsync方法中编写了一个无限循环,每次迭代都根据当前时间重绘时钟,并调用Jscex.Async.sleep方法“暂停”当前代码执行一秒钟时间。

既然环境没有提供阻塞代码的机制,我们又如何暂停代码执行?这里的奇妙之处在于:我们并没有真正执行刚才编写的函数,它已经由Jscex.compile方法编译成另一个形式了(暂时无需理解这段代码的含义):

 1 (function (interval) {
 2     var $$_builder_$$_0 = Jscex.builders["async"];
 3     return $$_builder_$$_0.Start(this,
 4         $$_builder_$$_0.Loop(
 5             function () {
 6                 return true;
 7             },
 8             null,
 9             $$_builder_$$_0.Delay(function () {
10                 drawClock(new Date());
11                 return $$_builder_$$_0.Bind(Jscex.Async.sleep(interval), function () {
12                     return $$_builder_$$_0.Normal();
13                 });
14             }),
15             false
16         )
17     );
18 })

Jscex编译器生成的新函数代码将由eval方法动态执行,并保存了当前作用域及上下文信息(例如变量、闭包等等)。

这里的$await方法是异步构造器里的bind操作,它指示编译器将后续的代码放入异步构造器的Bind方法的回调函数里。$await方法接受Jscex.Async.sleep的返回的异步任务作为参数,其语义是“等待该操作完成”。如果一个异步任务还未开始执行,则Bind方法也会启动这个任务。

从这个简单的示例上看,使用Jscex的实现似乎还更长一些。还且参考接下来的一些例子,它们会向您展示Jscex的真正魅力。

动画

动画是富用户界面的重要元素。假设我们需要编写一个动画,将某元素从一定时间内,从一个位置移动到另一个位置(samples/async/move.html )。如果使用传统的回调机制,则代码可能是这样编写的:

 1 var moveTraditionally = function (e, startPos, endPos, duration, callback) {
 2 
 3     var t = 0;
 4 
 5     // move a bit
 6     function move() {
 7         e.style.left = startPos.x + (endPos.x - startPos.x) * t / duration;
 8         e.style.top = startPos.y + (endPos.y - startPos.y) * t / duration;
 9 
10         t += 50;
11         if (t < duration) {
12             setTimeout(50, move);
13         } else { // finished
14             e.style.left = endPos.x;
15             e.style.top = endPos.y;
16             callback();
17         }
18     }
19 
20     setTimeout(50, move);
21 }

您能立即告诉我移动元素时所用的算法吗?反复阅读这段代码之后,也许我们可以知道这段代码的具体实现方式,但是对于程序员来说,这段代码的可读性和可写性都十分糟糕。不过有了Jscex之后,一切就完全不同了:

 1 var moveAsync = eval(Jscex.compile("async", function (e, startPos, endPos, duration) {
 2     for (var t = 0; t < duration; t += 50) {
 3         e.style.left = startPos.x + (endPos.x - startPos.x) * t / duration;
 4         e.style.top = startPos.y + (endPos.y - startPos.y) * t / duration;
 5         $await(Jscex.Async.sleep(50)); // 等待50毫秒
 6     }
 7 
 8     e.style.left = endPos.x;
 9     e.style.top = endPos.y;
10 }));

我们能够用传统的(线性)方式来表示算法:在for循环的每个迭代中休眠50毫秒并移动一小步。简单而优雅。

如今我们有了一个异步方法moveAsync,我们可以用它来构建其他的异步方法,就像核心库中的Jscex.Async.sleep方法一般,它们的返回值对外部来说,都是具有相同协议的“异步任务”。所以请看以下的方法:

 1 var moveSquareAsync = eval(Jscex.compile("async", function(e) {
 2     $await(moveAsync(e, {x:100, y:100}, {x:400, y:100}, 1000));
 3     $await(moveAsync(e, {x:400, y:100}, {x:400, y:400}, 1000));
 4     $await(moveAsync(e, {x:400, y:400}, {x:100, y:400}, 1000));
 5     $await(moveAsync(e, {x:100, y:400}, {x:100, y:100}, 1000));
 6 }));
 7 
 8 var box = document.getElementById("box");
 9 moveSquareAsync(box).start();

我想moveSquareAsync方法的意图已经足够明显了:它会将一个元素沿正方形的轨迹移动。在Jscex中组合异步操作实在是轻而易举的事情。

排序算法动画

每个程序员都懂排序算法,例如“冒泡排序”:

 1 var compare = function (x, y) {
 2     return x - y; 
 3 }
 4 
 5 var swap = function (array, i, j) {
 6     var t = array[x];
 7     array[x] = array[y];
 8     array[y] = t;
 9 }
10 
11 var bubbleSort = function (array) {
12     for (var x = 0; x < array.length; x++) {
13         for (var y = 0; y < array.length - x; y++) {
14             if (compare(array[y], array[y + 1]) > 0) {
15                 swap(array, y, y + 1);
16             }
17         }
18     }
19 }

用动画来表示排序算法的策略十分简单:在每次元素交换过后重绘图像,并暂停一小会儿。但是这个策略实现起来并没有听上去那么容易,例如,我们用setTimeout来“暂停”代码之后就无法用for来编写循环了。不过且看下面的Jscex版本:

 1 var compareAsync = eval(Jscex.compile("async", function (x, y) {
 2     $await(Jscex.Async.sleep(10)); // each "compare" takes 10 ms.
 3     return x - y;
 4 }));
 5 
 6 var swapAsync = eval(Jscex.compile("async", function (array, x, y) {
 7     var t = array[x];
 8     array[x] = array[y];
 9     array[y] = t;
10 
11     repaint(array); // repaint after each swap
12 
13     $await(Jscex.Async.sleep(20)); // each "swap" takes 20 ms.
14 }));
15 
16 var bubbleSortAsync = eval(Jscex.compile("async", function (array) {
17     for (var x = 0; x < array.length; x++) {
18         for (var y = 0; y < array.length - x; y++) {
19             var r = $await(compareAsync(array[y], array[y + 1]));
20             if(r > 0) {
21                 $await(swapAsync(array, y, y + 1));
22             }
23         }
24     }
25 }));

我想没有必要做更多解释,这完全是标准的“冒泡排序”算法。自然我们也可以实现“快速排序”:

 1 var _partitionAsync = eval(Jscex.compile("async", function (array, begin, end) {
 2     var i = begin;
 3     var j = end;
 4     var pivot = array[Math.floor((begin + end) / 2)];
 5 
 6     while (i <= j) {
 7         while (true) {
 8             var r = $await(compareAsync(array[i], pivot));
 9             if (r < 0) { i++; } else { break; }
10         }
11 
12         while (true) {
13             var r = $await(compareAsync(array[j], pivot));
14             if (r > 0) { j--; } else { break; }
15         }
16 
17         if (i <= j) {
18             $await(swapAsync(array, i, j));
19             i++;
20             j--;
21         }
22     }
23 
24     return i;
25 }));
26 
27 var _quickSortAsync = eval(Jscex.compile("async", function (array, begin, end) {
28     var index = $await(_partitionAsync(array, begin, end));
29 
30     if (begin < index - 1) {
31         $await(_quickSortAsync(array, begin, index - 1));
32     }
33 
34     if (index < end) {
35         $await(_quickSortAsync(array, index, end));
36     }
37 }));
38 
39 var quickSortAsync = eval(Jscex.compile("async", function (array) {
40     $await(_quickSortAsync(array, 0, array.length - 1));
41 }));

完整的示例在“samples/async/sorting-animations.html ”文件中。使用支持HTML 5的浏览器打开页面,点击上方的链接,您便能观察三种排序算法的动画:冒泡、选择与快速排序。

汉诺塔

汉诺塔 为一个在三根棍子之间移动盘子的谜题。它有一种简单的递归解决方案:

  1. 将上方n-1个盘子从A移动到B(借助C),此时只有第n个盘子留在A上。
  2. 将第n个盘子从A移动到C。
  3. 将n-1个盘子从B移动到C(借助A)。

用代码来表示便是:

1 var hanoi = function (n, from, to, mid) {
2     if (n > 0) hanoi(n - 1, from, mid, to);
3     moveDisc(n, from, to);
4     if (n > 0) hanoi(n - 1, mid, to from);
5 }
6 
7 hanoi(5, "A", "C", "B");

我们想将这个算法表示为动画(samples/async/hanoi.html ),则只需像上例那样重写一下代码:

 1 var hanoiAsync = eval(Jscex.compile("async", function(n, from, to, mid) {
 2     if (n > 0) {
 3         $await(hanoiAsync(n - 1, from, mid, to));
 4     }
 5 
 6     $await(moveDiscAsync(n, from, to));
 7 
 8     if (n > 0) {
 9         $await(hanoiAsync(n - 1, mid, to, from));
10     }
11 }));
12 
13 hanoiAsync(5, "A", "C", "B").start();

如果您在浏览器里打开页面,便会发现盘子会不断飞来飞去,自动解决谜题。不过这种方式似乎不太适合人们观察每一步行动,因此我们来做出一点变化:

 1 var hanoiAsync = eval(Jscex.compile("async", function(n, from, to, mid) {
 2     if (n > 0) {
 3         $await(hanoiAsync(n - 1, from, mid, to));
 4     }
 5 
 6     // wait for the button's being clicked
 7     var btnNext = document.getElementById("btnNext");
 8     $await(Jscex.Async.onEvent(btnNext, "click"));
 9 
10     $await(moveDiscAsync(n, from, to));
11 
12     if (n > 0) {
13         $await(hanoiAsync(n - 1, mid, to, from));
14     }
15 }));

在每次moveDiscAsync之前,程序需要“等待”按钮的“click”事件。在Jscex的异步类库中,“异步任务”的概念是“任何在未来完成的操作”。在这个例子里,按钮的“click”事件也是个异步任务──这个任务会在用户点击按钮之后完成 。于是程序继续执行,移动一个盘子,并等待下一次点击。

在异步程序中摆脱回调函数,这便是Jscex提高生产力及可维护性的法门。

简单的Node.js静态文件服务器

Jscex可以在任何支持ECMAScript 3的执行引擎里使用,不光是浏览器,服务器端环境亦可,例如Node.js。Node.js是一个充分利用异步驱动模型的服务器端JavaScript执行环境。这使得Node.js在许多互联网架构下能获得绝好的性能。

以下便是使用Node.js构造的一个简单的文件服务器:

 1 var http = require("http");
 2 var fs = require("fs");
 3 var url = require("url");
 4 var path = require("path");
 5 
 6 var transferFile = function(request, response) {
 7     var uri = url.parse(request.url).pathname;
 8     var filepath = path.join(process.cwd(), uri);
 9 
10     // check whether the file is exist and get the result from callback
11     path.exists(filepath, function(exists) {
12         if (!exists) {
13             response.writeHead(404, {"Content-Type": "text/plain"});
14             response.write("404 Not Found\n");
15             response.end();
16         } else {
17             // read the file content and get the result from callback
18             fs.readFile(filepath, "binary", function(error, data) {
19                 if (error) {
20                     response.writeHead(500, {"Content-Type": "text/plain"});
21                     response.write(error + "\n");
22                 } else {
23                     response.writeHead(200);
24                     response.write(data, "binary");
25                 }
26 
27                 response.end();
28             });
29         }
30     });
31 }
32 
33 http.createServer(function(request, response) {
34     transferFile(request, response);
35 }).listen(8124, "127.0.0.1");

上面的代码使用了两个异步方法:path.existsfs.readFile。Node.js的API大都是异步的,这大大提高了伸缩性,却也带来了编程难度。不过在Jscex的帮助下,我们编写异步程序就像普通的代码一样(samples/async/node-server.js):

 1 require("../../lib/uglifyjs-parser.js");
 2 require("../../src/jscex.js");
 3 require("../../src/jscex.async.js");
 4 require("../../src/jscex.async.node.js");
 5 
 6 Jscex.Async.Node.Path.extend(path);
 7 Jscex.Async.Node.FileSystem.extend(fs);
 8 
 9 var transferFileAsync = eval(Jscex.compile("async", function (request, response) {
10     var uri = url.parse(request.url).pathname;
11     var filepath = path.join(process.cwd(), uri);
12 
13     var exists = $await(path.existsAsync(filepath));
14     if (!exists) {
15         response.writeHead(404, {"Content-Type": "text/plain"});
16         response.write("404 Not Found\n");
17     } else {
18 
19         try {
20             var data = $await(fs.readFileAsync(filepath));
21             response.writeHead(200);
22             response.write(data, "binary");
23         } catch (ex) {
24             response.writeHead(500, {"Content-Type": "text/plain"});
25             response.write(ex + "\n");
26         }
27     }
28 
29     response.end();
30 }));
31 
32 http.createServer(function(request, response) {
33     transferFileAsync(request, response).start();
34 }).listen(8125, "127.0.0.1");

所有的Jscex文件都兼容 CommonJS 的模块规范,可以用require方法加载至Node.js中。Jscex异步编程库遵循一个简单的“异步任务”约定,人人都可以编写响应的扩展或适配器。详细信息请参考后续章节。

异步任务,及针对异步操作进行扩展

Jscex异步类库操作的是“异步任务”,每个“异步任务”都遵循Jscex.Async.Task类型所定义的协议,Jscex函数及内置的Jscex.Async.sleep等方法都会使用这个协议。每个任务对象都有如下成员:

  • start()方法:用于启动一个异步任务,这个方法只能在“ready”状态下使用。
  • addListener(onComplete)方法:在任务上注册一个“onComplete”监听器,它会在任务完成时调用(无论成功还是失败)。我们可以在“ready”及“running”状态下添加监听器。监听器函数在调用时会得到当前的异步任务对象作为其唯一的参数。
  • status字段:用于表明任务的状态。异步任务可处于以下四种状态:
    • ready:创建了任务,但还未开启。
    • running:已经调用start方法,任务正在运行。
    • succeeded:任务执行成功,可以通过result字段获得结果。
    • failed:任务执行失败,可以通过error字段获得错误对象。
  • result字段:获得任务的执行结果,请参考上方"succeeded"状态描述。
  • error字段:获得任务出错时的错误对象,请参考上方"failed"状态描述。

以下是个手动使用异步任务的例子:

 1 var someMethodAsync = eval(Jscex.compile("async", function () {
 2     // ...
 3 }));
 4 
 5 var task = someMethodAsync();
 6 task.addListener(function (t) {
 7     if (t.status == "succeeded") {
 8         console.log(t.result);
 9     } else if (t.status == "failed") {
10         console.log(t.error);
11     } else {
12         throw "Something got wrong in Jscex async library.";
13     }
14 });
15 
16 task.start();

自然,Jscex函数中的$await操作符也是通过异步任务的这些成员来工作的。

为异步操作编写扩展/适配器

异步操作无处不在,有可能是在浏览器环境里(如DOM事件)或是其他类库或框架中(如jQuery动画,Node.js的API等等)。人人都能轻易为现有的异步操作编写扩展/适配器。例如,我们可以扩展XMLHttpRequest对象以简化使用(src/jscex.async.xhr.js):

 1 XMLHttpRequest.prototype.receiveAsync = function () {
 2     var _this = this;
 3 
 4     var delegate = {
 5         "start": function (callback) {
 6             _this.onreadystatechange = function () {
 7                 if (_this.readyState == 4) {
 8                     callback("success", _this.responseText);
 9                 }
10             }
11 
12             _this.send();
13         }
14     };
15 
16     return new Jscex.Async.Task(delegate);
17 }
Jscex.Async.Task类型会接受一个异步操作的“委托”。这个委托对象上定义了一个start方法,接受一个callback回调函数。我们需要在异步操作完成时调用callback函数:
  • callback("success", result)意味着操作成功。
  • callback("failure", error)意味着操作出错。

再来一个例子。上面的Node.js示例使用了如下的定义的扩展(src/jscex.async.node.js):

 1 Jscex.Async.Node = {};
 2 Jscex.Async.Node.Path = {};
 3 Jscex.Async.Node.FileSystem = {};
 4 
 5 Jscex.Async.Node.Path.extend = function (path) {
 6 
 7     path.existsAsync = function (filepath) {
 8         var delegate = {
 9             "start": function (callback) {
10                 path.exists(filepath, function (exists) {
11                     callback("success", exists);
12                 });
13             }
14         };
15 
16         return new Jscex.Async.Task(delegate);
17     };
18 }
19 
20 Jscex.Async.Node.FileSystem.extend = function (fs) {
21 
22     fs.readFileAsync = function (filepath) {
23         var delegate = {
24             "start": function (callback) {
25                 fs.readFile(filepath, function (error, data) {
26                     if (error) {
27                         callback("failure", error);
28                     } else {
29                         callback("success", data);
30                     }
31                 });
32             }
33         };
34 
35         return new Jscex.Async.Task(delegate);
36     }
37 }

未来Jscex将为流行的JavaScript类库/框架提供更多扩展。

限制

目前Jscex主要有三个限制,不过从我的经验来看,它们都不是严重的问题:

需要独立的$await语句

Jscex编译器只能处理显式的$await操作:

  • 简单形式:$await(...);
  • 声明形式:var result = $await(...);
  • 赋值形式:this.result = $await(...);
  • 返回形式:return $await(...);

其他形式的使用方法都无法编译:

1 f(g(1), $await(...))
2 
3 if (x > y && $await(...)) { ... }

我们要将其放在独立的语句中:

1 var a1 = g(1);
2 var a2 = $await(...);
3 f(a1, a2);
4 
5 if (x > y) {
6     var flag = $await(...);
7     if (flag) { ... }
8 }

嵌套Jscex函数

如果您编写嵌套的Jscex函数,内层函数会与外层函数一齐编译。例如:

1 var outerAsync = eval(Jscex.compile("async", function () {
2 
3     var innerAsync = eval(Jscex.compile("async", function () {
4         // inner implementations
5     }));
6 
7 }));

在运行时,outsideAsync则会被编译为:

 1 (function () {
 2     var $$_builder_$$_0 = Jscex.builders["async"];
 3     return $$_builder_$$_0.Start(this,
 4         $$_builder_$$_0.Delay(function () {
 5 
 6             var innerAsync = (function () {
 7                 var $$_builder_$$_1 = Jscex.builders["async"];
 8                 return $$_builder_$$_1.Start(this,
 9                     // compiled inner implementations
10                     $$_builder_$$_1.Normal()
11                 );
12             });
13 
14             return $$_builder_$$_0.Normal();
15         })
16     );
17 })

这意味着每次执行outsideAsync函数时,innerAsync便会被编译一次,这是个不小的性能开销。因此,在这个问题解决之前请不要使用嵌套的Jscex函数。

不过(内层函数的)编译过程基于静态分析和代码生成,因此编译器只能识别出“标准模式”:eval(Jscex.compile("builderName", function () { ... }))。更多信息请参考“AOT编译器 - 限制”一节。

语言支持

Jscex编译器支持几乎完整的ECMAScript 3特性,除了以下两点:

  • 跳转到某个标签
  • switch语句中的“绑定”操作(例如$await),请使用if/else来代替。

AOT编译器

AOT(ahead-of-time)编译器同为一段JavaScript代码(scripts/jscexs.js),用于在运行时之前静态生成代码。

我们为什么需要AOT编译器

JIT编译器其实工作地很好。函数只会在页面加载或Node.js执行时编译一次,因此编译器的性能开销也很小。编译器的总大小在gzip后也只有大约10K,对我来说完全可以接受。

但是问题出在“脚本压缩”上。一般来说,网站使用的脚本都会被UglifyJS、Closure Compiler或YUI编译器压缩。如果仅仅是去除空白字符,缩短变量命名等方式,则Jscex编译器也能正常工作。但是现代的压缩器还会重新调整代码结构以得到更小的体积:

1 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))}}))

上面的代码是“冒泡排序”动画的代码,使用UglifyJS压缩后的结果。请注意Jscex不能处理d>0&&$await(...)这样的代码,因此我们必须在压缩前便进行编译。这便是构建AOT编译器的主要原因。

使用方式

我们使用Node.js来执行AOT编译器:

node scripts/jscexc.js --input input_file --output output_file

对于之前的bubbleSortAsync方法,它则会被编译为:

 1 (function (array) {
 2     var $$_builder_$$_0 = Jscex.builders["async"];
 3     return $$_builder_$$_0.Start(this,
 4         $$_builder_$$_0.Delay(function () {
 5             var x = 0;
 6             return $$_builder_$$_0.Loop(
 7                 function () {
 8                     return x < array.length;
 9                 },
10                 function () {
11                     x++;
12                 },
13                 $$_builder_$$_0.Delay(function () {
14                     var y = 0;
15                     return $$_builder_$$_0.Loop(
16                         function () {
17                             return y < (array.length - x);
18                         },
19                         function () {
20                             y++;
21                         },
22                         $$_builder_$$_0.Delay(function () {
23                             return $$_builder_$$_0.Bind(compareAsync(array[y], array[y + 1]), function (r) {
24                                 if (r > 0) {
25                                     return $$_builder_$$_0.Bind(swapAsync(array, y, y + 1), function () {
26                                         return $$_builder_$$_0.Normal();
27                                     });
28                                 } else {
29                                     return $$_builder_$$_0.Normal();
30                                 }
31                             });
32                         }),
33                         false
34                     );
35                 }),
36                 false
37             );
38         })
39     );
40 })

AOT编译器会保持Jscex函数外的代码不变。AOT编译器生成的代码可以被安全地压缩。静态编译后的代码可以脱离“json2.js”,“uglifyjs-parser.js”及“jscex.js”独立执行。异步方法只需要依赖“jscex.async.js”即可,gzip之后只有3K左右大小。此外,静态编译后的代码可以在未来ECMAScript 5的“严格模式”下执行(严格模式不支持eval方法)。

限制

AOT编译器会对输入脚本进行静态解析,因此它只能识别标准模式:eval(Jscex.compile("builderName", function () { ... }))。下面的代码可以在JIT编译器下正常工作,但无法通过AOT编译器:

1 var compile = Jscex.compile;
2 var builderName = "async";
3 var func = function () { ... };
4 var newCode = compile(builderName, func);
5 var funcAsync = eval(newCode);

索性“标准模式”已足够使用,这个限制并不会造成真正的问题。

不仅仅是异步

Jscex不仅面向异步编程。Jscex编译器将输入的函数转化为标准的monadic形式,剩下的工作由“Jscex构造器”负责。Jscex内置一个“异步构造器”,用于简化异步编程,我们也可以定义一个“序列构造器”,可以使用JavaScript(ECMAScript 3)创建“迭代器”──正如JavaScript 1.7中的“generator”特性 或是Python/C#中的类似功能。如下:

1 var rangeSeq = eval(Jscex.compile("seq", function (minInclusive, maxExclusive) {
2     for (var i = minInclusive; i < maxExclusive; i++) {
3         $yield(i);
4     }
5 }));

Jscex构造器都注册在Jscex.builders字典中。在运行时会根据名称找到构造器,您可以从console.log的输出,或是AOT编译器里查看其编译结果:

 1 (function (minInclusive, maxExclusive) {
 2     var $$_builder_$$_0 = Jscex.builders["seq"];
 3     return $$_builder_$$_0.Start(this,
 4         $$_builder_$$_0.Delay(function () {
 5             var i = minInclusive;
 6             return $$_builder_$$_0.Loop(
 7                 function () {
 8                     return i < maxExclusive;
 9                 },
10                 function () {
11                     i++;
12                 },
13                 $$_builder_$$_0.Delay(function () {
14                     return $$_builder_$$_0.Bind(i, function () {
15                         return $$_builder_$$_0.Normal();
16                     });
17                 }),
18                 false
19             );
20         })
21     );
22 })

可惜目前“序列构造器”并不是Jscex的一部分,它是Jscex项目未来的计划之一。

与其他项目的比较

目前在JavaScript编程领域也有一些与Jscex目的类似的项目,它们往往可以分为两类。

类库扩展

大部分项目通过构建类库的方式来简化JavaScript异步编程 ,例如:

 1 // async.js (https://github.com/fjakobs/async.js)
 2 async.list([
 3     asncFunction1,
 4     asncFunction2,
 5     asncFunction3,
 6     asncFunction4,
 7     asncFunction5,
 8 ]).call().end(function(err, result) {
 9     // do something useful
10 });
11 
12 // Step (https://github.com/creationix/step)
13 Step(
14     function readSelf() {
15         fs.readFile(__filename, this);
16     },
17     function capitalize(err, text) {
18         if (err) throw err;
19         return text.toUpperCase();
20     },
21     function showIt(err, newText) {
22         if (err) throw err;
23         console.log(newText);
24     }
25 );

开发人员在使用这些解决方案时需要遵循类库或框架定义的编程模式,而Jscex完全遵循JavaScript的惯用法,使用Jscex编程与传统JavaScript编程毫无二至,学习成本极低。

语言扩展

第二类解决方案是以 Narrative JavaScriptStratified JS 为代表的“语言扩展”。开发人员编写“类JavaScript”代码,并将其编译为真正的JavaScript。例如:

 1 // Narrative JavaScript code
 2 function sleep(millis) {
 3     var notifier = new EventNotifier();
 4     setTimeout(notifier, millis);
 5     notifier.wait->();
 6 }
 7 
 8 // StratifiedJS code
 9 var result;
10 waitfor {
11   result = performGoogleQuery(query);
12 }
13 or {
14   result = performYahooQuery(query);
15 }

尽管这些语言可以做到同JavaScript十分相似,但它们终究是一门新的语言,尤其是与“异步”相关的语法及语义。开发人员必须学习一门“新的”语言才能开始工作,而您的JavaScript编辑器可能也无法正常处理这些语法。但Jscex完全就是JavaScript,即便是$await操作的表现形式也是简单的方法调用,唯一相关的语义也仅仅是“等待其结束”而已,人人都能在几分钟内掌握Jscex。

此外,传统的JavaScript编程体验是:人们修改代码以后直接刷新页面便能看到修改后的结果。但这些“新语言”不能直接在浏览器(或其他ECMAScript 3执行引擎)里使用,它们必须在运行时前编译成JavaScript。虽然Narrative和StratifiedJS同样提供了JIT编译器,可以在运行时加载并生成代码,但它们的做法也有明显的局限性:

如果源代码由XMLHttpRequest加载得到,那么这些源文件则必须与网站放在相同的域名下。JSONP可用来跨域名加载代码,但它并非直接加载远程代码,而是加载一种“方法调用”的形式,这意味着源代码需要进行一定修改才能被客户端所使用。

这些项目也可以直接读取并编译页面上的script代码块,但如果使用这种方式:

1 <!-- codes to compile -->
2 <script type="some-special-type">
3     // 定义一个异步函数
4 </script>
5 
6 <script type="text/javascript">
7     // cannot use the async method here
8 </script>

开发人员无法在下方的代码块中直接使用之前定义的异步方法,这与传统的JavaScript编程体验不符。

但Jscex的一切都和JavaScipt保持一致:

  1. 没有明显的“编译期”和“运行时”之分,JIT编译器可以在运行时生成新代码。
  2. Jscex函数是标准的JavaScript,可以使用普通的JavaScript编辑器来编写和格式化代码。
  3. Jscex代码就定义在普通的JavaScript源文件里,它可以放在任意的域名下供网页使用。
  4. Jscex函数在定义后可以立即使用。

总而言之,Jscex几乎提供了JavaScript程序员所拥有的一切体验,使得程序员能够以原有的开发方式,轻松地开发异步JavaScript应用程序。

深入Jscex

(暂无)

未来计划

  • 更好的异步构造器
    • 更好的性能
    • 支持异步任务的“取消”操作
    • 更多Jscex.Async基本操作
  • 支持“序列构造器”
  • 为流行的JavaScript类库/框架(如jQuery,Node.js等等)提供扩展。

相关项目

  • F#:Jscex项目受其神奇的“计算表达式”及“异步工作流”特性启发而来。
  • C# vNext:未来的C#提供了如F#异步工作流及Jscex这样绝佳的异步编程体验。异步构造器的$await名称便取自C# vNext的“await”操作符。
  • UglifyJS:Jscex编译器使用了UglifyJS的解析器:简单,高效,并支持ECMAScript 3引擎。
  • Narcissus:Narcissus是由JavaScript编写的JavaScript解释器,基于SpiderMonkey引擎。它的解析器比UglifyJS慢得多,还依赖于SpiderMonkey的部分扩展。不过它生成的AST包含丰富的信息,对于AOT编译器来说十分有用。
  • Closure Compiler:用于压缩脚本。

Bug及反馈

遇到bug或任何反馈请即时联系我,您可以使用 Google群组 或直接给我写邮件。

授权协议

Jscex基于BSD授权协议开源:

Copyright 2011 (c) Jeffrey Zhao <jeffz@live.com>
Based on UglifyJS (https://github.com/mishoo/UglifyJS).

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

    * Redistributions of source code must retain the above
      copyright notice, this list of conditions and the following
      disclaimer.

    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials
      provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.