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 |