UDN-企业互联网技术人气社区

板块导航

浏览  : 585
回复  : 4

[Nodejs] 从地狱到天堂,Node 回调向 async/await 转变

[复制链接]
葡萄柚的头像 楼主
发表于 2017-1-5 15:55:50 | 显示全部楼层 |阅读模式
  Node7 通过 --harmony_async_await 参数开始支持 async/await,而 async/await 由于其可以以同步形式的代码书写异步程序,被喻为异步调用的天堂。然而 Node 的回调模式在已经根深蒂固,这个被喻为“回调地狱”的结构形式推动了 Promise 和 ES6 的迅速成型。然而,从地狱到天堂,并非一步之遥!

  async/await 基于 Promise,而不是基于回调,所以要想从回调地狱中解脱出来,首先要把回调实现修改为 Promise 实现——问题来了,Node 这么多库函数,还有更多的第三方库函数都是使用回调实现的,要想全部修改为 Promise 实现,谈何容易?

  使用第三方库脱离地狱

  Async

  当然,解决办法肯定是有的,比如 Async 库通过 async.waterfall() 实现了对深度回调的“扁平”化,当然它不是用 Promise 实现的,但是有它的扁平化工作作为基础,再封装 Promise 就已经简洁不少了。

  下面是 Async 官方文档给出的一个示例
  1. async.waterfall([
  2.     function(callback) {
  3.         callback(null, 'one', 'two');
  4.     },
  5.     function(arg1, arg2, callback) {
  6.         // arg1 now equals 'one' and arg2 now equals 'two'
  7.         callback(null, 'three');
  8.     },
  9.     function(arg1, callback) {
  10.         // arg1 now equals 'three'
  11.         callback(null, 'done');
  12.     }
  13. ], function (err, result) {
  14.     // result now equals 'done'
  15. });
复制代码

  如果把它封装成 Promise 也很容易:
  1. // promiseWaterfall 使用 async.waterfall 处理函数序列
  2. // 并将最终结果封装成 Promise
  3. function promiseWaterfall(series) {
  4.     return new Promise((resolve, reject) => {
  5.         async.waterfall(series, function(err, result) {
  6.             if (err) {
  7.                 reject(err);
  8.             } else {
  9.                 resolve(result);
  10.             }
  11.         });
  12.     });
  13. }

  14. // 调用示例
  15. promiseWaterfall([
  16.     function(callback) {
  17.         callback(null, "one", "two");
  18.     },
  19.     function(arg1, arg2, callback) {
  20.         // arg1 now equals 'one' and arg2 now equals 'two'
  21.         callback(null, "three");
  22.     },
  23.     function(arg1, callback) {
  24.         // arg1 now equals 'three'
  25.         callback(null, "done");
  26.     }
  27. ]).then(result => {
  28.     // result now equals 'done'
  29. });
复制代码

  Q

  Q 也是一个常用的 Promise 库,提供了一系列的工具函数来处理 Node 式的回调,比如 Q.nfcall() 、 Q.nfapply() 、 Q.denodeify() 等。

  其中, Q.denodeify() ,别名 Q.nfbind() ,可以将一个 Node 回调风格的函数转换成 Promise 风格的函数。虽然转换之后的函数返回的不是原生的 Promise 对象,而是 Q 内部实现的一个 Promise 类的对象,我们可以称之为 Promise alike 对象。

  Q.denodeify() 的用法很简单,直接对 Node 风格的函数进行封装即可,下面也是官方文档中的例子
  1. var readFile = Q.nfbind(FS.readFile);
  2. readFile("foo.txt", "utf-8").done(function (text) {
  3.     // do something with text
  4. });
复制代码

  这里需要说明的是,虽然用 Q.denodeify() 封装的函数返回的是 Promise alike 对象,但是笔者亲测它可以用于 await 运算 [注1] 。

  [注1] :await 在 MDN 上被描述为 “operator”,即运算符,所以这里说 “await 运算”,或者可以说 “await 表达式”。

  Bluebird

  对于 jser 来说, Bluebird 也不陌生。它通过 Promise.promisify() 和 Promise.promisifyAll() 等提供了对 Node 风格函数的转换,这和上面提到的 Q.denodeify() 类似。注意这里提到的 Promise 也不是原生的 Promise,而是 bluebird 实现的,通常使用下面的语句引用:
  1. const Promise = require("bluebird").Promise;
复制代码

  为了和原生 Promise 区别开来,也可以改为
  1. 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 风格的函数,所以不管是参数还是返回值,都是函数
  1. // promisify 的结构
  2. function promisify(func) {
  3.     return function() {
  4.         // ...
  5.     };
  6. }
复制代码

  [2]> 返回的函数需要返回 Promise 对象

  既然 promisify() 的返回值是一个 Promise 风格的函数,它的返回值应该是一个 Promise 对象,所以
  1. function promisify(func) {
  2.     return function() {
  3.         return new Promise((resolve, reject) => {
  4.             // TODO
  5.         });
  6.     };
  7. }
复制代码

  [3]> Promise 中调用 func

  毋庸置疑,上面的 TODO 部分需要实现对 func 的调用,并根据结果适当的调用 resolve() 和 reject() 。
  1. function promisify(func) {
  2.     return function() {
  3.         return new Promise((resolve, reject) => {
  4.             func((err, result) => {
  5.                 if (err) {
  6.                     reject(err);
  7.                 } else {
  8.                     resolve(result);
  9.                 }
  10.             });
  11.         });
  12.     };
  13. }
复制代码

  Node 回调风格的回调函数第一个参数都是错误对象,如果为 null 表示没有错误,所以会有 (err, result) => {} 这样的回调定义。

  [4]> 加上参数

  上面调用还没有加上对参数的处理。对于 Node 回调风格的函数,通常前面 n 个参数是内部实现需要使用的参数,而最后一个参数是回调函数。使用 ES6 的可变参数和扩展数据语法很容易实现
  1. // 最终实现如下
  2. function promisify(func) {
  3.     return function(...args) {
  4.         return new Promise((resolve, reject) => {
  5.             func(...args, (err, result) => {
  6.                 if (err) {
  7.                     reject(err);
  8.                 } else {
  9.                     resolve(result);
  10.                 }
  11.             });
  12.         });
  13.     };
  14. }
复制代码

  至此,完整的 promisify() 就实现出来了。

  [5]> 实现 promisifyArray()

  promisifyArray() 用于批量处理一组函数,参数是回调风格的函数列表,返回对应的 Promise 风格函数列表。在实现了 promisify() 的基础上实现 promisifyArray() 非常容易。
  1. function promisifyArray(list) {
  2.     return list.map(promisify);
  3. }
复制代码

  [6]> 实现 promisifyObject()

  promisifyObject() 的实现需要考虑 this 指针的问题,相对比较复杂,而且也不能直接使用上面的 promisify() 。下面是 promisifyObject() 的简化实现,详情参考代码中的注释。
  1. function promisifyObject(obj, suffix = "Promisified") {
  2.     // 参照之前的实现,重新实现 promisify。
  3.     // 这个函数没用到外层的局部变量,不必实现为局域函数,
  4.     // 这里实现为局部函数只是为了组织演示代码
  5.     function promisify(func) {
  6.         return function(...args) {
  7.             return new Promise((resolve, reject) => {
  8.                 // 注意调用方式的变化
  9.                 func.call(this, ...args, (err, result) => {
  10.                     if (err) {
  11.                         reject(err);
  12.                     } else {
  13.                         resolve(result);
  14.                     }
  15.                 });
  16.             });
  17.         };
  18.     }

  19.     // 先找出所有方法名称,
  20.     // 如果需要过滤可以考虑自己加 filter 实现
  21.     const keys = [];
  22.     for (const key in obj) {
  23.         if (typeof obj[key] === "function") {
  24.             keys.push(key);
  25.         }
  26.     }

  27.     // 将转换之后的函数仍然附加到原对象上,
  28.     // 以确保调用的时候,this 引用正确。
  29.     // 为了避免覆盖原函数,加了一个 suffix。
  30.     keys.forEach(key => {
  31.         obj[`${key}${suffix}`] = promisify(obj[key]);
  32.     });

  33.     return obj;
  34. }
复制代码

  天堂就在眼前

  脱离了地狱,离天堂就不远了。我在之前的博客 理解 JavaScript 的 async/await 已经说明了 async/await 和 Promise 的关系。而上面已经使用了大量的篇幅实现了回调风格函数向 Promise 风格函数的转换,所以接下来要做的就是 async/await 实践。

  把 promisify 相关函数封装成模块

  既然是在 Node 中使用,前面自己实现的 promisify() 、 promisifyArray() 和 promisifyObject() 还是封装在一个 Node 模块中比较好。前面已经定义好了三个函数,只需要导出就好
  1. module.exports = {
  2.     promisify: promisify,
  3.     promisifyArray: promisifyArray,
  4.     promisifyObject: promisifyObject
  5. };

  6. // 通过解构对象导入
  7. // const {promisify, promisifyArray, promisifyObject} = require("./promisify");
复制代码

  因为三个函数都是独立的,也可以导出成数组,
  1. module.exports = [promisify, promisifyArray, promisifyObject];

  2. // 通过解构数组导入
  3. // const [promisify, promisifyArray, promisifyObject] = require("./promisify");
复制代码

  模拟一个应用场景

  这个模拟的应用场景里需要进行一个操作,包括4个步骤 (均为异步操作)

  • first()
  • second()
  • third()
  • last()

  其中第 2 、 3 步可以并行。

  这个场景用到的数据结构定义如下
  1. class User {
  2.     constructor(id) {
  3.         this._id = id;
  4.         this._name = `User_${id}`;
  5.     }

  6.     get id() {
  7.         return this._id;
  8.     }

  9.     get name() {
  10.         return this._name;
  11.     }

  12.     get score() {
  13.         return this._score || 0;
  14.     }

  15.     set score(score) {
  16.         this._score = parseInt(score) || 0;
  17.     }

  18.     toString() {
  19.         return `[#${this._id}] ${this._name}: ${this._score}`;
  20.     }
  21. }
复制代码

  使用 setTimeout 来模拟异步

  定义一个 toAsync() 来将普通函数模拟成异步函数。可以少写几句 setTimeout() 。
  1. function toAsync(func, ms = 10) {
  2.     setTimeout(func, ms);
  3. }
复制代码

  以回调风格模拟4个步骤
  1. function first(callback) {
  2.     toAsync(() => {
  3.         // 产生一个 1000-9999 的随机数作为 ID
  4.         const id = parseInt(Math.random() * 9000 + 1000);
  5.         callback(null, id);
  6.     });
  7. }

  8. function second(id, callback) {
  9.     toAsync(() => {
  10.         // 根据 id 产生一个 User 对象
  11.         callback(null, new User(id));
  12.     });
  13. }

  14. function third(id, callback) {
  15.     toAsync(() => {
  16.         // 根据 id 计算一个分值
  17.         // 这个分值在 50-100 之间
  18.         callback(null, id % 50 + 50);
  19.     });
  20. }

  21. function last(user, score, callback) {
  22.     toAsync(() => {
  23.         // 将分值填入 user 对象
  24.         // 输出这个对象的信息
  25.         user.score = score;
  26.         console.log(user.toString());
  27.         if (callback) {
  28.             callback(null, user);
  29.         }
  30.     });
  31. }
复制代码

  当然,还有导出
  1. module.exports = [first, second, third, last];
复制代码

  async/await 实践
  1. const [promisify, promisifyArray, promisifyObject] = require("./promisify");

  2. const [first, second, third, last] = promisifyArray(require("./steps"));

  3. // 使用 async/await 实现
  4. // 用 node 运行的时候需要 --harmoney_async_await 参数
  5. async function main() {
  6.     const userId = await first();

  7.     // 并行调用要用 Promise.all 将多个并行处理封装成一个 Promise
  8.     const [user, score] = await Promise.all([
  9.         second(userId),
  10.         third(userId)
  11.     ]);
  12.     last(user, score);
  13. }

  14. main();
复制代码

相关帖子

发表于 2017-1-5 15:56:19 | 显示全部楼层
感觉JavaScript很有前途
使用道具 举报

回复

发表于 2017-1-5 15:56:20 | 显示全部楼层
感觉JavaScript很有前途
使用道具 举报

回复

发表于 2017-1-5 15:56:21 | 显示全部楼层
回帖支持下楼主,请眼熟我,我叫“第一权势“
使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部