Continuation.js——解决JavaScript的异步难题
Continuation.js是一个JavaScript的CPS(Continuation-Passing Style)变换工具,用于简化异步JavaScript编程。Continuation.js是一个把「同步」JavaScript编译为标准JavaScript的编译器,引入了虚拟的函数cont,以便于更容易地写异步的代码。cont不是真正的函数,是一个与函数语法相同的变换标记。使用Continuation.js你可以像顺序的代码一样写出异步的代码,然后由编译器编译为异步调用的风格(延续传递风格,Continuation-Passing Style,CPS)。
概览
通常用JavaScript写异步的代码十分困难,因为一不小心就会写出这样的代码:
function textProcessing(callback) {
fs.readFile('somefile.txt', 'utf-8', function (err, contents) {
if (err) return callback(err);
//process contents
contents = contents.toUpperCase();
fs.readFile('somefile2.txt', 'utf-8', function (err, contents2) {
if (err) return callback(err);
contents += contents2;
fs.writeFile('somefile_concat_uppercase.txt', contents, function (err) {
if (err) return callback(err);
callback(null, contents);
});
});
});
}
textProcessing(function (err, contents) {
if (err)
console.error(err);
});
这种风格的代码被成为「回调陷阱」或「回调金字塔」。使用Continuation.js,可以直接写成:
function textProcessing(ret) {
fs.readFile('somefile.txt', 'utf-8', cont(err, contents));
if (err) return ret(err);
contents = contents.toUpperCase();
fs.readFile('somefile2.txt', 'utf-8', cont(err, contents2));
if (err) return ret(err);
contents += contents2;
fs.writeFile('somefile_concat_uppercase.txt', contents, cont(err));
if (err) return ret(err);
ret(null, contents);
}
textProcessing(cont(err, contents));
if (err)
console.error(err);
上面这段代码通过一个虚拟的函数「cont」展平了回调金字塔。当控制流执行到异步的调用时,会等待fs.readFile「返回」,cont参数表里面的变量会被赋值为结果。返回在这里有一些歧义,确切地说是回调函数被调用,不同于「函数返回」的字面意思。因为异步的函数通常会立刻返回,而回调函数可能等到操作执行完成以后才被调用。可以简单理解成cont后面,直到函数结束的部分都是当前异步调用的回调函数。这样的代码令人感觉是顺序执行的,实际上执行中仍然是异步。
更简单地,还可以配合obtain使用JavaScript的try..catch语法。obtain(a)相当于cont(err, a),如果err不是undefined,会被throw抛出。
function textProcessing(ret) {
try {
fs.readFile('somefile.txt', 'utf-8', obtain(contents));
contents = contents.toUpperCase();
fs.readFile('somefile2.txt', 'utf-8', obtain(contents2));
contents += contents2;
fs.writeFile('somefile_concat_uppercase.txt', contents, obtain());
ret(null, contents);
} catch(err) {
ret(err);
}
}
try {
textProcessing(obtain(contents));
} catch(err) {
console.error(err);
}
特性
- JIT(Just-in-time)和AOT(Ahead-of-time)编译器
- 没有任何运行时的依赖
- 没有额外语法,只有cont,obtain,parallel三个保留字
- 自由的编码风格,编译后的代码依然可读
- 兼容CoffeeScript和LiveScript(以及其他编译到js的语言)
- 支持Node.js和浏览器端JavaScript
- 支持并行化和轻量级线程
文档
cont
cont是一个异步调用的标记,用于接受异步返回的结果,必须用在函数调用的参数表中,代替回调函数的位置。cont的参数表中的变量会被设置为异步的回调函数的参数,作为返回值。如果cont参数表中有变量(而非表达式),变量会被自动定义。
例子:
setTimeout(cont(), 1000);
fs.lstat('/path/file', cont(err, stats));
var obj;
fs.readdir('/path', cont(err, obj.files));
编译后的代码:
var err, stats, obj;
setTimeout(function () {
fs.lstat('/path', function () {
err = arguments[0];
stats = arguments[1];
fs.readdir('/path', function () {
err = arguments[0];
obj.files = arguments[1];
});
});
}, 1000);
obtain
obtain是一个cont和throw的语法糖。可以使用try..catch来捕获obtain抛出的异常。使用obtain的一个假设是回调函数的第一个参数是错误对象,如果没有错误则被设置为undefined或者null,(这是Node.js API的一个约定)。
例子:
function f1() {
fs.readdir('/path', obtain(files));
}
function f2() {
fs.readdir('/path', cont(err, files));
if (err)
throw err;
}
编译后的代码:
function f1() {
var _$err, files;
fs.readdir('/path', function () {
_$err = arguments[0];
files = arguments[1];
if (_$err)
throw _$err;
});
}
function f2() {
var err, files;
fs.readdir('/path', function () {
err = arguments[0];
files = arguments[1];
if (err) {
throw err;
}
});
}
parallel
parallel可以让异步的函数「并行」地执行。parallel也是一个虚拟的函数调用,它的参数必须都是一个带cont或obtain的函数调用。参数中所有函数调用会并行地执行,直到所有的并行函数都执行结束后(确切地说是回调函数被调用),控制流才继续向后执行。
注意无论是Node.js还是浏览器JavaScript,代码都是单线程执行的,所以parallel并不是一个系统级别的多线程的实现。由于函数调用很像一个线程的入口,所以可以称为「轻量级线程」。只有I/O和计算之间可以并行执行,计算和计算仍然是顺序的。可以理解为当一个线程被I/O阻塞时,另一个就可以运行了,这些「线程」可以自动调度利用计算和I/O的空隙,以实现并行。所有如果要用parallel,请确保「线程」中以I/O为主,而不是大量的计算。
例子:
var contents = {};
parallel(
fs.readFile('/path1', obtain(contents.file1)),
fs.readFile('/path2', obtain(contents.file2)),
fs.readFile('/path3', obtain(contents.file3))
);
console.log(contents);
显式声明模式
Continuation.js可以自动递归编译使用require调用的模块,如果在需要编译的文件加上'use continuation',同时使用continuation script.js --explicit运行模块,只有标注了'use continuation'的模块才会被编译。使用这个选项可以加快模块载入,但要确保每个需要编译的文件上加上'use continuation'。
编译缓存
使用contination script.js --cache [cacheDir]运行时,所有编译过的模块会被缓存到cacheDir,下一次再运行时就可以直接取用。如果源文件的时间戳新于缓存,那么就会被重新编译。这个选项依赖于系统时间戳,使用它也可以大幅度减少载入时间。推荐--explicit和--cache同时使用。
默认cacheDir是/tmp/continuation
使用CoffeeScript(以及其他编译到js的语言)
Continuation.js于绝大部分编译到js的语言兼容,因为没有引入任何新的语法,仅有的三个关键字cont,obtain和parallel都与函数调用的语法相同,因此可以直接使用对应语言的函数调用语法。
例子(CoffeeScript):
dns = require('dns')
domains = ['www.google.com', 'nodejs.org', 'www.byvoid.com']
for domain in domains
dns.resolve domain, obtain(addresses)
console.log addresses
目前Continuation.js内建支持CoffeeScript和LiveScript。
在程序中使用Continuation.js
Continuation.js支持作为模块调用,提供一个compile(code)接口,code是表示代码的字符串。
例子:
var continuation = require('continuation');
function fibonacci() {
var a = 0, current = 1;
while (true) {
var b = a;
a = current;
current = a + b;
setTimeout(cont(), 1000);
console.log(current);
}
};
var code = fibonacci.toString();
var compiledCode = continuation.compile(code);
console.log(compiledCode);
eval(compiledCode);
fibonacci();
以上这段代码可以直接用node命令运行,无需安装Continuation.js到全局环境。这段代码把一个函数转换为字符串,然后调用Continuation.js编译,最后再通过eval运行。
安装
通过npm安装Continuation.js:
npm install -g continuation
使用
Usage: continuation [options] <file.js/file.coffee/file.ls> [arguments]
Options:
-h, --help output usage information
-V, --version output the version number
-p, --print compile script file and print it
-o, --output <filename> compile script file and save as <filename>
-e, --explicit compile only if "use continuation" is explicitly declared
-c, --cache [directory] run and cache compiled sources to [directory], by default [directory] is /tmp/continuation
-v, --verbose print verbosal information to stderr
直接使用Continuation.js运行代码(例如script.js):
contination script.js
将编译后的代码输出到终端:
contination script.js -p
将编译后的代码保存到另一个文件:
contination script.js -o compiled.js
使用显式标记模式运行代码:(只编译带有'use continuation'的代码):
contination script.js -e
开启编译缓存:
contination script.js -c
例子
循环和延时
计算Fibonacci数列,每秒钟输出一个:
var fib = function () {
var a = 0, current = 1;
while (true) {
var b = a;
a = current;
current = a + b;
setTimeout(cont(), 1000);
console.log(current);
}
};
fib();
顺序执行异步代码
顺序读5个文件
var fs = require('fs');
for (var i = 0; i < 4; i++) {
fs.readFile('text' + i + '.js', 'utf-8', obtain(text));
console.log(text);
}
console.log('Done');
并行执行异步函数
var fs = require('fs');
var dns = require('dns');
var http = require('http');
var complexWork = function (next) {
setTimeout(cont(), 500);
http.get('https://www.byvoid.com', cont(res));
next(null, res.headers);
};
parallel(
fs.readdir('/', obtain(files)),
dns.resolve('npmjs.org', obtain(addresses)),
complexWork(obtain(result))
);
console.log(files, addresses, result);
递归
计算目录所占磁盘空间:
var fs = require('fs');
function calcDirSize(path, callback) {
var dirSize = 0, dirBlockSize = 0;
fs.readdir(path, obtain(files));
for (var i = 0; i < files.length; i++) {
var filename = path + '/' + files[i];
fs.lstat(filename, obtain(stats));
if (stats.isDirectory()) {
calcDirSize(filename, obtain(subDirSize, subDirBlockSize));
dirSize += subDirSize;
dirBlockSize += subDirBlockSize;
} else {
dirSize += stats.size;
dirBlockSize += 512 * stats.blocks;
}
}
callback(null, dirSize, dirBlockSize);
}
var path = process.argv[2];
if (!path) path = '.';
calcDirSize(path, obtain(totalSize, totalBlockSize));
console.log('Size:', Math.round(totalSize / 1024), 'KB');
console.log('Actual Size on Disk:', Math.round(totalBlockSize / 1024), 'KB');
更多的例子可以在代码'examples'和'test'目录中找到。
相关的项目
Continuation.js不是惟一的CPS变换工具,有一些其他的解决方案。下面是一个简单的比较:
项目 | Continuation.js | streamline.js | TameJS | Wind.js | jwacs | NarrativeJS | StratifiedJS |
---|---|---|---|---|---|---|---|
Node.js支持 | 是 | 是 | 是 | 是 | 否 | 否 | 是 |
浏览器支持 | 是 | 是 | 否 | 是 | 是 | 是 | 是 |
运行时依赖 | 否 | 是 | 是 | 是 | 否 | 是 | 是 |
额外语法 | 否 | 否 | 是 | 否 | 是 | 是 | 是 |
兼容编译到js的语言 | 是 | 是 (CoffeeScript) | 是 (CoffeeScript) | 是 (手动) | 否 | 否 | 否 |
并行支持 | 是 | 是 | 是 | 是 | 是 | 未知 | 是 |
生成的代码可读性 | 是 | 否 | 困难 | 几乎可读 | 未知 | 否 | 未知 |
异步结果传递方式 | 参数表 | 返回值 | 参数表 | 返回值 | 返回值 | 返回值 | 返回值 |
文档 | 有 | 有 | 有 | 不详 | 有 | 有 | 有 |
实现 | JavaScript | JavaScript | JavaScript | JavaScript | Lisp | Java | JavaScript |