异步任务运行
在第八章中,我介绍了生成器,并向你展示了如何使用它来运行异步任务, 就像这样:
let fs = require("fs");
function run(taskDef) {
// create the iterator, make available elsewhere
let task = taskDef();
// start the task
let result = task.next();
// recursive function to keep calling next()
function step() {
// if there's more to do
if (!result.done) {
if (typeof result.value === "function") {
result.value(function(err, data) {
if (err) {
result = task.throw(err);
return;
}
result = task.next(data);
step();
});
} else {
result = task.next(result.value);
step();
}
}
}
// start the process
step();
}
// Define a function to use with the task runner
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}
// Run a task
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});
此实现存在一些痛点。首先,将每个函数包裹在另一个函数内、再返回一个新函数,这是有点令人困惑的(这句话本身就已经够乱了)。 其次,返回值为函数的情况下,没有任何方法可以区分它是否应当被作为任务运行器的回调函数。
借助 Promise,你可以确保每个异步操作都返回一个 Promise,从而大幅度简化并一般化异步处理,通用接口也意味着你可以大大减少异步代码。此处有一个简化任务运行器的方式:
let fs = require("fs");
function run(taskDef) {
// create the iterator
let task = taskDef();
// start the task
let result = task.next();
// recursive function to iterate through
(function step() {
// if there's more to do
if (!result.done) {
// resolve to a promise to make it easy
let promise = Promise.resolve(result.value);
promise.then(function(value) {
result = task.next(value);
step();
}).catch(function(error) {
result = task.throw(error);
step();
});
}
}());
}
// Define a function to use with the task runner
function readFile(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, function(err, contents) {
if (err) {
reject(err);
} else {
resolve(contents);
}
});
});
}
// Run a task
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});
在此版本的代码中,一个通用的 run() 函数执行了生成器来创建一个迭代器。它调用了 task.next() 来启动任务,并递归调用 step() 直到迭代完成。
在 step() 函数内部,如果还有更多工作要做,那么 result.done 的值会是 false,此时 result.value 应当是一个 Promise,不过调用 Promise.resolve() 只为预防未正确返回 Promise 的函数(记住:Promise.resolve() 在被传入任意 Promise 时只会直接将其传递回来,而不是 Promise 的参数则会被包装为 Promise)。接下来,一个完成处理函数被添加以便提取该 Promise 值,并将该值传回迭代器。此后,在 step() 函数调用自身之前,result 被赋值为下一个 yield 的结果。
|
Promise.resolve() 在被传入任意 Promise 时只会直接将其传递回来 |
一个拒绝处理函数将任意拒绝结果存储在一个错误对象中。task.throw() 方法将这个错误对象传回给迭代器,而若一个错误在任务中被捕获,result 也会被赋值为下一个 yield 的结果,这样 step() 最终在 catch() 内部就会被调用,以便继续任务执行。
run() 函数能运行任意使用 yield 来实现异步代码的生成器,而不会将 Promise(或回调函数)暴露给开发者。事实上,由于函数调用后的返回值总是会被转换为一个 Promise,该函数甚至允许返回 Promise 之外的类型。这意味着同步与异步方法在使用 yield 时都会正常工作,并且你永不需要检查返回值是否为一个 Promise。
唯一需要担心的是,要确保诸如 readFile() 的异步方法能返回一个正确标记其状态的 Promise。对于 Node.js 内置的方法来说,这意味着你必须转换这些方法,让它们返回 Promise 而不是使用回调函数。
|
未来的异步任务运行 在我写这本书的时候,针对 JS 中的异步任务运行,为之引入简单语法的一项工作正在进行。此工作开展在 await 语法上,极度借鉴了上述以 Promise 为基础的例子。其基本理念是使用一个被 async 标记的函数(而非生成器),并在调用另一个函数时使用 await 而非 yield,就像这样:
在 function 之前的 async 关键字标明了此函数使用异步方式运行。await 关键字则表示对于 readFile("config.json") 的函数调用应返回一个 Promise,若返回类型不对,则会将其包装为 Promise。与上述 run() 的实现一致,await 会在 Promise 被拒绝的情况下抛出错误,否则它将返回该 Promise 被决议的值。最终结果是你可以将异步代码当作同步代码来书写,而无须为管理基于迭代器的状态机而付出额外开销。 await 语法预计将在 ES2017(即 ES8)中被最终敲定。(译注:已被纳入 ES8) |