为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
JavaScript

Node7.6 开始正式支持 async/await,而 async/await 由于其可以以同步形式的代码书写异步程序,被喻为异步调用的天堂。然而 Node 的回调模式在已经根深蒂固,这个被喻为“回调地狱”的结构形式推动了 Promise 和 ES6 的迅速成型。然而,从地狱到天堂,并非一步之遥!

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

使用第三方库脱离地狱

Async

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

下面是 Async 官方文档给出的一个示例

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 也很容易:

// promiseWaterfall 使用 async.waterfall 处理函数序列// 并将最终结果封装成 Promisefunction 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

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

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

Q.denodeify() 的用法很简单,直接对 Node 风格的函数进行封装即可,下面也是官方文档中的例子

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 表达式”。

Bluebird

对于 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 实践。

把 promisify 相关函数封装成模块

既然是在 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个步骤 (均为异步操作)

  1. first() 获得一个用户 ID

  2. second() 根据用户 ID 获取用户的信息

  3. third() 根据用户 ID 获取用户的分数

  4. last() 输出用户信息和分数

其中第 23 步可以并行。

这个场景用到的数据结构定义如下

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}`;    }}

使用 setTimeout 来模拟异步

定义一个 toAsync() 来将普通函数模拟成异步函数。可以少写几句 setTimeout()

function toAsync(func, ms = 10) {    setTimeout(func, ms);}

以回调风格模拟4个步骤

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];

async/await 实践

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();

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消