异步任务运行
在第八章中,我介绍了生成器,并向你展示了如何使用它来运行异步任务, 就像这样:
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) |