Node.js教程 如何解决回调嵌套及串行状态传送问题
沉沙 2018-07-26 来源 : 阅读 1404 评论 0

摘要:本篇Node.js教程探讨了如何解决回调嵌套及串行状态传送问题,希望阅读本篇文章以后大家有所收获,帮助大家对Node.js的理解更加深入。

引子

在使用Node/JS编程的时候,经常会遇到这样的问题:有一连串的异步方法,需要按顺序执行,前后结果之间有依赖关系,形如(片断1):

asyncTask(initial, function (err, result) {//step 1

    if (err)

        throw err;

    asyncTask(result, function (err, result2) {//step 2

        if (err)

            throw err;

        asyncTask(result2, function (err, result3) {//final

            if (err)

                throw err;

            console.log(result3);

        });

    });

});

Promise

解决回调嵌套及串行状态传送问题,是有规范可循的,如 CommonJS的Promise规范 。

Promise是对异步编程的一种抽象。它是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常。

以实现较多的 Promise/A(thenable)来说:

· Promise作为一个对象,代表了一次任务的执行,它有三种状态:成功/失败/执行中

· Promise对象有一个 then接口(原型函数),形如:then(fulfilledHandler, errorHandler, progressHandler)。这三个参数在Promise对象完成任务时被执行,它们对应状态为成功/失败/执行中——我们可以把then方法等同于Promise对象的构造器,fulfilledHandler、errorHandler及progressHandler可以对应到Promise.resolve(val)、Promise.reject(reason)及Promise.notify(update)

· Promise对象还有一个catch接口(原型函数),形如:catch(onRejected)。onRejected为错误处理的回调,接收从Promise.reject(reason)传递过来的错误信息

· Promise对象暴露给调用方的then接口,是一个永远可以返回Promise对象自身的函数,所以then可以继续链式调用then,直到任务完成——简单说,Promise执行的结果可以传递给下一个Promise对象,这在异步任务的串行化中非常有用(并且我们不用担心这些Promise在执行任务时产生副作用,根据规范,每一个Promise与被调函数间都是相互独立的

· Promise对象的内部,维护一个队列,在构造执行链完成时,将待执行函数存入需要依次执行的任务——什么时候算完成呢?一般的Promise库实现,都需要在构造链的尾部调用一个done之类的函数,以明示构造链的终结。

Promise做为类和对象的细节,MDN上的描述更为详尽。回到实现层面,先来看一个开源的Promise框架,Q:

Q

Q是一个对Promise/A规范实现较为完备的开源框架。

针对前述的代码片断1 场景,Q提供了这样的可能性:Q.promise能将异步逻辑包装成一个thenable函数,从而注入它实现的回调函数,源码形如:

Q.promise = promise;

function promise(resolver) {

    if (typeof resolver !== "function") {

        throw new TypeError("resolver must be a function.");

    }

    var deferred = defer();

    try {

        resolver(deferred.resolve,

            deferred.reject, deferred.notify);

    } catch (reason) {

        deferred.reject(reason);

    }

    return deferred.promise;

}

 这个resolver就是我们的异步逻辑封装函数, 我们可以选择性的接收resolve和reject作为参数,并在异步方法完成/出错时进行回调,让Q获得流程控制权。

Q的异步串行例子

假设有两个文本文件:1.txt 和 2.txt,内容分别为:I'm the first text.\n和I'm the second text.\n。我们需要顺序且异步的读取它们,在全部读取完成/出错时,显示相应信息。

首先,需要将异步方法进行包装(片断2):

var Q = require('q');

var path = require('path');

var fs = require('fs');

function readFile(previous, fileName) {

    return Q.promise(function (resolve, reject) {

        fs.readFile(path.join(process.cwd(), fileName),

            function (error, text) {

                if (error) {

                    reject(new Error(error));

                } else {

                    resolve(previous + text.toString());

                }

            });

    });

}

fs.readFile 读取文件,成功调用Q.defer.resolve,出错调用Q.defer.reject。readFile做为用于串联Promise对象的方法,提供了一个previous状态参数,用于累加上次执行的结果。有了这个方法,基于then的串行逻辑就能这样实现:

readFile('', '1.txt')

    .then(function (previous) {

        return readFile(previous, '2.txt');

    })

    .then(function (finalText) {

        console.log(finalText);

    })

    .catch(function (error) {

        console.log(error);

    })

    .done();

可以看出,thenable函数的链式调用总是能将上一个Promise.resolve的结果做为参数传入。

Async

Async 严格说起来不是一个Promise的实现,而是一个异步工具集(utilities),通过源码我们能看得很清楚,它导出了非常多的方法,集合/流程控制 都有涉及。

针对前述的代码片断1 场景,Async提供了若干种方法,挑两个有代表性的:

Async.waterfall

waterfall形如waterfall(tasks, [callback]),tasks是一个function数组,[callback]参数是最终结果的回调。tasks数组里的函数按顺序执行,当前任务可以接收上一个任务执行的结果,看个例子:

var async = require('async');

var path = require('path');

var fs = require('fs');

function readFile4WaterFall(previous, fileName, callback) {

    fs.readFile(path.join(process.cwd(), fileName),

        function (error, text) {

            callback(error, previous + text.toString());

        });

}

async.waterfall(

    [

        function (callback) {

            readFile4WaterFall('', '1.txt', callback)

        },

        function (previous, callback) {

            readFile4WaterFall(previous, '2.txt', callback);

        }

    ], function (err, result) {

        console.log(result);

    }

);

可以看出,不管是何种形式的异步流程控制,都需要注入实现的回调(这里是function(callback)),以获取流程控制权。运行结果:

I'm the first text.

I'm the second text.

Async.series

series形如series(tasks, [callback]),和waterfall不同,tasks数组里的函数按顺序执行,每个任务只接受一个callback注入,并不能传递上一次任务执行的结果。每一个函数执行的结果,都被push到了一个result数组里,也就是[callback(error, result)]的第二个参数。例子:

var async = require('async');

var path = require('path');

var fs = require('fs');

function readFile4Series(fileName, callback) {

    fs.readFile(path.join(process.cwd(), fileName),

        function (error, text) {

            callback(error, text.toString());

        });

}

async.series(

    [

        function (callback) {

            readFile4Series('1.txt', callback)

        },

        function (callback) {

            readFile4Series('2.txt', callback);

        }

    ], function (err, result) {

        console.log(result);

    }

);

运行结果:

[ 'I\'m the first text.\n', 'I\'m the second text.\n' ]

动态的异步串行

老实说,上面的示例仅仅展示了异步框架的威力,却并不实用:在实践中,我们遇到的情况并不是事先构造好要执行的函数链,而是代码动态决定要执行哪些函数,即 .then 是动态拼接出来的。

以前示Q的片断2 为例,要读取的文件名存在数组里,我们需要针对文件名构造执行链:

//要读取的文件数组

var files = ['1.txt', '2.txt', '3.txt'];

//要构造的Promise链

var tasks = [];

readFile高阶函数需要稍加改造,因为不是显式的构造链,原 .then 传递上次执行的函数需要嵌入到高阶函数中:

function readFileDynamic(fileName) {

    return function(previous) { //.then callback

        return Q.promise(function (resolve, reject) {

            fs.readFile(path.join(process.cwd(), fileName),

                function (error, text) {

                    if (error) {

                        reject(new Error(error));

                    } else {

                        resolve(previous + text.toString());

                    }

                });

        });

    }

}

构造任务链和执行链:

files.forEach(function (fileName) {

    tasks.push(readFileDynamic(fileName));

});

var result = Q('');

tasks.forEach(function (f) {

    result = result.then(f);

});

调用:

result

    .then(function (finalText) {

        console.log(finalText);

    })

    .catch(function (error) {

        console.log(error);

    })

    .done();

如此,借助Q就可实现动态的异步串行链,本质和静态构造执行链无二致,只是Promise构造形式进行了转换。至于Async就更简单了,前述的 waterfall/series的 tasks本就是个数组,天然动态。


本文由职坐标整理发布,学习更多的Node.js相关知识,请关注职坐标WEB前端Node.js频道!

本文由 @沉沙 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程