Node7 经过 --harmony_async_await
参数开始支持 async/await,而 async/await 因为其能够以同步形式的代码书写异步程序,被喻为异步调用的天堂。然而 Node 的回调模式在已经根深蒂固,这个被喻为“回调地狱”的结构形式推进了 Promise 和 ES6 的迅速成型。然而,从地狱到天堂,并不是一步之遥!javascript
async/await 基于 Promise,而不是基于回调,因此要想从回调地狱中解脱出来,首先要把回调实现修改成 Promise 实现——问题来了,Node 这么多库函数,还有更多的第三方库函数都是使用回调实现的,要想所有修改成 Promise 实现,谈何容易?html
固然,解决办法确定是有的,好比 Async 库经过 async.waterfall()
实现了对深度回调的“扁平”化,固然它不是用 Promise 实现的,可是有它的扁平化工做做为基础,再封装 Promise
就已经简洁很多了。java
下面是 Async 官方文档给出的一个示例node
async.waterfall([ function(callback) { callback(null, 'one', 'two'); }, function(arg1, arg2, callback) { // arg1 now equals 'one' and arg2 now equals 'two' callback(null, 'three'); }, function(arg1, callback) { // arg1 now equals 'three' callback(null, 'done'); } ], function (err, result) { // result now equals 'done' });
若是把它封装成 Promise
也很容易:git
// promiseWaterfall 使用 async.waterfall 处理函数序列 // 并将最终结果封装成 Promise function promiseWaterfall(series) { return new Promise((resolve, reject) => { async.waterfall(series, function(err, result) { if (err) { reject(err); } else { resolve(result); } }); }); } // 调用示例 promiseWaterfall([ function(callback) { callback(null, "one", "two"); }, function(arg1, arg2, callback) { // arg1 now equals 'one' and arg2 now equals 'two' callback(null, "three"); }, function(arg1, callback) { // arg1 now equals 'three' callback(null, "done"); } ]).then(result => { // result now equals 'done' });
Q 也是一个经常使用的 Promise 库,提供了一系列的工具函数来处理 Node 式的回调,好比 Q.nfcall()、Q.nfapply()、Q.denodeify() 等。github
其中,Q.denodeify()
,别名 Q.nfbind()
,能够将一个 Node 回调风格的函数转换成 Promise 风格的函数。虽然转换以后的函数返回的不是原生的 Promise
对象,而是 Q 内部实现的一个 Promise
类的对象,咱们能够称之为 Promise alike 对象。segmentfault
Q.denodeify()
的用法很简单,直接对 Node 风格的函数进行封装便可,下面也是官方文档中的例子api
var readFile = Q.nfbind(FS.readFile); readFile("foo.txt", "utf-8").done(function (text) { // do something with text });
这里须要说明的是,虽然用 Q.denodeify()
封装的函数返回的是 Promise alike 对象,可是笔者亲测它能够用于 await 运算[注1]
。数组
[注1]
:await 在 MDN 上被描述为 “operator”,即运算符,因此这里说 “await 运算”,或者能够说 “await 表达式”。promise
对于 jser 来讲,Bluebird 也不陌生。它经过 Promise.promisify() 和 Promise.promisifyAll() 等提供了对 Node 风格函数的转换,这和上面提到的 Q.denodeify()
相似。注意这里提到的 Promise
也不是原生的 Promise,而是 bluebird 实现的,一般使用下面的语句引用:
const Promise = require("bluebird").Promise;
为了和原生 Promise 区别开来,也能够改成
const BbPromise = require("bluebird").Promise;
Promise.promisifyAll()
相对特殊一些,它接受一个对象做为参数,将这个对象的全部方法处理成 Promise 风格,固然你也能够指定一个 filter 让它只处理特定的方法——具体操做这里就很少说,参考官方文档便可。
与 Q.denodeify()
相似,经过 bluebird 的 Promise.promisify()
或 Promise.promisifyAll()
处理事后的函数,返回的也是一个 Promise alike 对象,并且,也能够用于 await 运算。
ES6 已经提供了原生 Promise 实现,若是只是为了“脱离地狱”而去引用一个第三方库,彷佛有些不值。若是只须要少许代码就能够本身把回调风格封装成 Promise 风格,干吗不本身实现一个?
不妨分析一下,本身写个 promisify()
须要作些什么
[1]>
定义 promisify()
promisify()
是一个转换函数,它的参数是一个回调风格的函数,它的返回值是一个 Promise 风格的函数,因此不论是参数仍是返回值,都是函数
// promisify 的结构 function promisify(func) { return function() { // ... }; }
[2]>
返回的函数须要返回 Promise
对象既然 promisify()
的返回值是一个 Promise 风格的函数,它的返回值应该是一个 Promise
对象,因此
function promisify(func) { return function() { return new Promise((resolve, reject) => { // TODO }); }; }
[3]>
Promise 中调用 func
毋庸置疑,上面的 TODO
部分须要实现对 func
的调用,并根据结果适当的调用 resolve()
和 reject()
。
function promisify(func) { return function() { return new Promise((resolve, reject) => { func((err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); }; }
Node 回调风格的回调函数第一个参数都是错误对象,若是为 null
表示没有错误,因此会有 (err, result) => {}
这样的回调定义。
[4]>
加上参数上面调用尚未加上对参数的处理。对于 Node 回调风格的函数,一般前面 n 个参数是内部实现须要使用的参数,而最后一个参数是回调函数。使用 ES6 的可变参数和扩展数据语法很容易实现
// 最终实现以下 function promisify(func) { return function(...args) { return new Promise((resolve, reject) => { func(...args, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); }; }
至此,完整的 promisify()
就实现出来了。
[5]>
实现 promisifyArray()
promisifyArray()
用于批量处理一组函数,参数是回调风格的函数列表,返回对应的 Promise 风格函数列表。在实现了 promisify()
的基础上实现 promisifyArray()
很是容易。
function promisifyArray(list) { return list.map(promisify); }
[6]>
实现 promisifyObject()
promisifyObject()
的实现须要考虑 this
指针的问题,相对比较复杂,并且也不能直接使用上面的 promisify()
。下面是 promisifyObject()
的简化实现,详情参考代码中的注释。
function promisifyObject(obj, suffix = "Promisified") { // 参照以前的实现,从新实现 promisify。 // 这个函数没用到外层的局部变量,没必要实现为局域函数, // 这里实现为局部函数只是为了组织演示代码 function promisify(func) { return function(...args) { return new Promise((resolve, reject) => { // 注意调用方式的变化 func.call(this, ...args, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); }; } // 先找出全部方法名称, // 若是须要过滤能够考虑本身加 filter 实现 const keys = []; for (const key in obj) { if (typeof obj[key] === "function") { keys.push(key); } } // 将转换以后的函数仍然附加到原对象上, // 以确保调用的时候,this 引用正确。 // 为了不覆盖原函数,加了一个 suffix。 keys.forEach(key => { obj[`${key}${suffix}`] = promisify(obj[key]); }); return obj; }
脱离了地狱,离天堂就不远了。我在以前的博客 理解 JavaScript 的 async/await 已经说明了 async/await 和 Promise 的关系。而上面已经使用了大量的篇幅实现了回调风格函数向 Promise 风格函数的转换,因此接下来要作的就是 async/await 实践。
既然是在 Node 中使用,前面本身实现的 promisify()
、promisifyArray()
和 promisifyObject()
仍是封装在一个 Node 模块中比较好。前面已经定义好了三个函数,只须要导出就好
module.exports = { promisify: promisify, promisifyArray: promisifyArray, promisifyObject: promisifyObject }; // 经过解构对象导入 // const {promisify, promisifyArray, promisifyObject} = require("./promisify");
由于三个函数都是独立的,也能够导出成数组,
module.exports = [promisify, promisifyArray, promisifyObject]; // 经过解构数组导入 // const [promisify, promisifyArray, promisifyObject] = require("./promisify");
这个模拟的应用场景里须要进行一个操做,包括4个步骤 (均为异步操做)
first()
得到一个用户 IDsecond()
根据用户 ID 获取用户的信息third()
根据用户 ID 获取用户的分数last()
输出用户信息和分数其中第 2
、3
步能够并行。
class User { constructor(id) { this._id = id; this._name = `User_${id}`; } get id() { return this._id; } get name() { return this._name; } get score() { return this._score || 0; } set score(score) { this._score = parseInt(score) || 0; } toString() { return `[#${this._id}] ${this._name}: ${this._score}`; } }
定义一个 toAsync()
来将普通函数模拟成异步函数。能够少写几句 setTimeout()
。
function toAsync(func, ms = 10) { setTimeout(func, ms); }
function first(callback) { toAsync(() => { // 产生一个 1000-9999 的随机数做为 ID const id = parseInt(Math.random() * 9000 + 1000); callback(null, id); }); } function second(id, callback) { toAsync(() => { // 根据 id 产生一个 User 对象 callback(null, new User(id)); }); } function third(id, callback) { toAsync(() => { // 根据 id 计算一个分值 // 这个分值在 50-100 之间 callback(null, id % 50 + 50); }); } function last(user, score, callback) { toAsync(() => { // 将分值填入 user 对象 // 输出这个对象的信息 user.score = score; console.log(user.toString()); if (callback) { callback(null, user); } }); }
固然,还有导出
module.exports = [first, second, third, last];
const [promisify, promisifyArray, promisifyObject] = require("./promisify"); const [first, second, third, last] = promisifyArray(require("./steps")); // 使用 async/await 实现 // 用 node 运行的时候须要 --harmoney_async_await 参数 async function main() { const userId = await first(); // 并行调用要用 Promise.all 将多个并行处理封装成一个 Promise const [user, score] = await Promise.all([ second(userId), third(userId) ]); last(user, score); } main();