Jscex异步模块中提供了Jscex支持异步编程的核心类库,例如Task类型或是异步构造器。有了异步模块,我们就可以编写异步方法,或是将某种异步操作封装为Task对象等等。但是,异步模块除了提供了核心功能以外,并没有对日常异步开发中的常见任务给与足够的支持。而Jscex异步增强模块便对常见的异步开发模式,或是异步操作的绑定模式提供了支持。
如果您要使用Jscex异步增强模块,首先必须引入Jscex异步模块,此时您会得到了一个Jscex根对象,之后再基于这个对象初始化异步增强模块。
目前Jscex异步增强模块尚未发布至npm,因此您需要下载该文件,保存在合适的位置,并使用require
方法引入该本地模块并初始化Jscex根对象:
require("./jscex-async-powerpack").init(Jscex);
该方法也适用于非Node.js,但实现了CommonJS规范的JavaScript运行环境。
如果您要在浏览器里使用Jscex异步增强模块,请下载该文件,保存在合适的位置,并在页面中引入该文件即可:
<script src="jscex-async-powerpack.js"></script>
此时异步增强模块会自动为根上的Jscex对象添加异步增强模块相关的成员。该方法也适用于各类没有实现CommonJS规范的JavaScript运行环境。
异步增强模块中提供了一些常见的异步方法,可直接使用。这些异步方法都定义在Jscex.Async
模块之上。
sleep
方法用于将当前的异步方法暂停一段时间。该方法接受两个参数:
delay
:表示暂停时长,单位为毫秒。ct
:可选参数,用于取消暂停操作的CancellationToken对象。
该异步方法没有返回值。
使用示例:
var ct = new Jscex.Async.CancellationToken();
var printEverySecondAsync = eval(Jscex.compile("async", function (texts, ct) {
for (var i = 0; i < texts.length; i++) {
$await(Jscex.Async.sleep(1000, ct));
console.log(texts[i]);
}
}));
onEvent
方法用于监听某个对象上某个事件的“下一次”触发。该方法接受三个参数:
target
:目标对象。eventName
:事件名。ct
:可选参数,用于取消监听操作的CancellationToken对象。
为了保证兼容性,onEvent
会使用目标对象上的addEventListener
、addListener
或是attachEvent
方法来监听事件,并在操作结束或是取消之后使用removeEventListener
、removeListener
或是detachEvent
方法来取消监听。onEvent
将会返回事件对象,即事件触发时传递给监听器的第一个参数。
使用示例:
var Async = Jscex.Async;
var ct = new Async.CancellationToken();
var drawLinesAsync = eval(Jscex.compile("async", function (board, ct) {
var currPoint = $await(Async.onEvent(board, "click", ct));
while (true) {
var nextPoint = $await(Async.onEvent(board, "click", ct));
drawLine(
{ x: currPoint.clientX, y: currPoint.clientY },
{ x: nextPoint.clientX, y: nextPoint.clientY });
currPoint = nextPoint;
}
}));
Jscex异步增强模块也包含了一些常见的任务协作方式,它们作为静态方法定义在Jscex.Async.Task
类型上。
whenAll
方法接受一系列的Task对象,并返回一个新的Task对象,该新对象只有在所有Task都成功结束之后才会结束,并使用数组返回所有Task对象的结果,其顺序与输入Task的顺序一一对应。
使用示例:
var Task = Jscex.Async.Task;
var getUserItemsAsync = eval(Jscex.compile("async", function (userId) {
var results = $await(Task.whenAll(
queryUserAsync(userId),
queryItemsAsync(userId)
));
return {
user: results[0],
items: results[1]
};
});
在启动whenAll
返回的新Task对象时,会立即启动所有输入中尚未开始执行的Task对象。当任意一个输入Task对象发生错误时,新Task对象也会立即地直接抛出该异常,而其余正在运行的Task不受任何影响,无论成功还是失败都会执行完毕。
whenAll
方法亦可接受一个Task对象数组taskArray
作为参数,其余表现与上述重载完全相同。
使用示例:
var getUserItemsAsync = eval(Jscex.compile("async", function (userId) {
var tasks = [queryUserAsync(userId), queryItemsAsync(userId)];
var results = $await(Task.whenAll(tasks));
return {
user: results[0],
items: results[1]
};
});
whenAll
方法的第三个重载可以接受一个对象taskMap
作为参数,该对象上的每个字段都对应一个Task对象。新Task对象的返回值也是一个对象,包含所有Task对象的运行结果,并与输入对象的字段一一对应。该方法的其他表现与之前的两个重载完全相同。
使用示例:
var getUserItemsAsync = eval(Jscex.compile("async", function (userId) {
return $await(Task.whenAll({
user: queryUserAsync(userId),
items: queryItemsAsync(userId)
}));
});
whenAny
方法可以接受一系列的Task对象,并返回一个新的Task对象,该新对象会在输入的Task中任意一个完成时(无论成功失败)结束。该方法的返回值为一个对象,其key
字段为任务在输入时的下标,其task
对象即为第一个完成的任务。
例如,我们可以在Node.js中使用pipe
方法传递数据流内的数据:
var fs = require("fs");
var streamIn = fs.createReadStream("input.txt");
var streamOut = fs.createWriteStream("output.txt");
streamIn.pipe(streamOut);
此时可能会有三种情况发生:
streamOut
的close
事件触发,表示正常结束。streamOut
的error
事件触发,表示输出流异常。streamIn
的error
事件触发,表示输入流异常。
理论上来说,三种情况每次只会发生一种,因此我们可以编写这样的代码:
var fs = require("fs");
var Async = Jscex.Async;
var Task = Async.Task;
var copyFileAsync = eval(Jscex.compile("async", function (src, target) {
var streamIn = fs.createReadStream("input.txt");
var streamOut = fs.createWriteStream("output.txt");
streamIn.pipe(streamOut);
var any = $await(Task.whenAny(
Async.onEvent(streamOut, "close"),
Async.onEvent(streamOut, "error"),
Async.onEvent(streamIn, "error")
));
// 如果不是第一个输入完成,则意味着出错了
if (any.key != 0) {
throw any.task.result;
}
}));
在启动whenAny
返回的新Task对象时,会立即启动所有输入中尚未开始执行的Task对象。由于“完成”不分成功失败,因此whenAny
返回的新对象永远不会抛出异常。
whenAny
方法亦可接受一个Task对象数组taskArray
作为参数,其余表现与上述重载完全相同。
使用示例:
var fs = require("fs");
var Async = Jscex.Async;
var Task = Async.Task;
var copyFileAsync = eval(Jscex.compile("async", function (src, target) {
var streamIn = fs.createReadStream("input.txt");
var streamOut = fs.createWriteStream("output.txt");
streamIn.pipe(streamOut);
var tasks = [
Async.onEvent(streamOut, "close"),
Async.onEvent(streamOut, "error"),
Async.onEvent(streamIn, "error")];
var any = $await(Task.whenAny(tasks));
// 如果不是第一个输入完成,则意味着出错了
if (any.key != 0) {
throw any.task.result;
}
}));
whenAny
方法的第三个重载可以接受一个对象taskMap
作为参数,该对象上的每个字段都对应一个Task对象。与之前的两个重载相比,新Task对象的返回值中的key
字段保存了完成的那个Task对象所对应的键值。该方法的其他表现与之前的两个重载完全相同。
使用示例:
var fs = require("fs");
var Async = Jscex.Async;
var Task = Async.Task;
var copyFileAsync = eval(Jscex.compile("async", function (src, target) {
var streamIn = fs.createReadStream("input.txt");
var streamOut = fs.createWriteStream("output.txt");
streamIn.pipe(streamOut);
var any = $await(Task.whenAny({
end: Async.onEvent(streamOut, "close"),
errorOut: Async.onEvent(streamOut, "error"),
errorIn: Async.onEvent(streamIn, "error")
}));
// 如果完成的任务不是end,则意味着出错了
if (any.key != "end") {
throw any.task.result;
}
}));
如果要在Jscex异步方法中使用现有的异步操作,则需要将其绑定为Jscex中标准的Task对象。一般来说,绑定一个异步操作是一件简单直观的事情,但是像Node.js中提供了大量的异步操作,将其一一绑定也是一件不小的工作量。幸运的是,同一平台内的异步操作都基本具有相同的模式,我们可以通过编写一些的简单的辅助方法,来统一完成绑定操作。
Jscex异步增强模块也包含了一些绑定常见异步接口的辅助方法,它们都定义在Jscex.Async.Jscexify
模块下面。
某些异步操作会直接使用回调函数返回结果,例如Node.js中Path模块的exists
方法:
var path = require("path");
path.exists("/etc/passwd", function (exists) {
// exists参数表示是否存在
});
此时,便可以使用fromCallback
将此类异步操作绑定为一个返回Task对象的异步方法:
var Jscexify = Jscex.Async.Jscexify;
path.existsAsync = Jscexify.fromCallback(path.exists);
// 某Jscex异步方法内部
var exists = $await(path.existsAsync("/etc/passwd"));
某些异步操作会为回调函数传入多个参数,例如:
var split = function (numbers, n, callback) {
var smaller = 0, larger = 0, equals = 0;
for (var i = 0; i < numbers.length; i++) {
var num = numbers[i];
if (num == n) equals ++;
else if (num > n) larger ++;
else smaller ++;
}
callback(equals, larger, smaller);
}
此时,也可以在fromCallback
内逐个指定参数名称,这样最后Task对象的结果将会是一个包含这些字段的对象:
var splitAsync = Jscexify.fromCallback(split, "equals", "larger", "smaller");
// 某Jscex异步方法内部
var result = $await(splitAsync([1, 2, 3, 4, 5, 6], 3));
console.log(result.equals); // 1
console.log(result.smaller); // 2
console.log(result.larger); // 3
当然,如果您只关注回调函数的第一个参数,甚至一个都不关注,自然也可以在使用fromCallback
的时候不列出参数名称。
fromCallback
支持的是以回调形式返回结果的异步操作,而fromStandard
返回的便是“可能会失败”的“标准接口”,它会将回调函数的第一个参数作为错误对象,如果存在这个对象,则意味着该异步操作出现了错误,于是将其当作异常对象抛出。而真正的“返回值”,则是回调函数收到的第二个参数。
例如在Node.js中,绝大部分接口都遵守了这样的模式,例如File System模块下的许多方法:
var fs = require("fs");
fs.readdir(path, function (err, files) { … });
fs.stat(path, function (err, stats) { … });
fs.mkdir("./world", function (err) { … });
此时我们便可以使用fromStandard
创建这些异步操作的绑定:
fs.readdirAsync = Jscexify.fromStandard(fs.readdir);
fs.statAsync = Jscexify.fromStandard(fs.stat);
fs.mkdirAsync = Jscexify.fromStandard(fs.mkdir);
绑定后的异步方法在使用时可能会抛出异常:
try {
$await(fs.mkdir("./hello-world"));
} catch (ex) {
// 出错了
}
与fromCallback
一样,如果回调函数拥有多个返回值(即除了第一个表示错误的参数以外,还拥有两个或以上的参数),也可以在fromStandard
后列出参数名,例如Node.js中的Child Processes模块有个exec
方法:
var exec = require('child_process').exec;
exec('ls -l', function (error, stdout, stderr) {
if (error) {
console.log('exec error: ' + error);
} else {
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
}
});
我们可以将其绑定为:
var execAsync = Jscexify.fromStandard(exec, "stdout", "stderr");
// 某Jscex异步方法内
try {
var result = $await(execAsync("ls -l"));
console.log("stdout: " + result.stdout);
console.log("stderr: " + result.stderr);
} catch (ex) {
console.log('exec error: ' + exec);
}
当然,如果您只关注回调函数除了错误外的第一个参数,甚至一个都不关注,自然也可以在使用fromStandard
的时候不列出参数名称。