避免 nodeJs 中的回调地狱/将变量传递给内部函数

Posted

技术标签:

【中文标题】避免 nodeJs 中的回调地狱/将变量传递给内部函数【英文标题】:Avoiding callback hell in nodeJs / Passing variables to inner functions 【发布时间】:2014-11-14 11:12:39 【问题描述】:

这是一个我想简化的例子:

exports.generateUrl = function (req, res) 
    var id = req.query.someParameter;

    var query = MyMongooseModel.findOne('id': id);
    query.exec(function (err, mongooseModel) 
        if(err) 
            //deal with it
        

        if (!mongooseModel) 
            generateUrl(Id,
                function (err, text, url) 
                    if (err) 
                        res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
                        return;
                    
                    var newMongooseModel = new AnotherMongooseModel();
                    newMongooseModel.id = id;

                    newMongooseModel.save(function (err) 
                        if (err) 
                            res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
                         else 
                            res.send(url: url, text: text);
                        
                    );
                );
         else 
            //deal with already exists
        
    );
;

我看过其他 SO 回答,他们告诉您使用命名函数,但没有说明如何处理您想要传入的变量或使用 jQuery 的队列。这两个我都没有。

我知道我可以用名称函数替换我的匿名函数,但是我需要传递变量。例如,如果函数在别处定义,我的内部函数将如何访问 res

【问题讨论】:

【参考方案1】:

您的问题的核心是:

我知道我可以用名称函数替换我的匿名函数,但是我需要传递变量。例如,如果函数在其他地方定义,我的内部函数将如何访问 res?

答案是使用函数工厂。

一般来说是这样的:

function x (a) 
    do_something(function()
        process(a);
    );

可以转换成这样:

function x (a) 
    do_something(y_maker(a)); // notice we're calling y_maker,
                              // not passing it in as callback


function y_maker (b) 
    return function () 
        process(b);
    ;

在上面的代码中,y_maker 是一个生成函数的函数(我们称该函数的用途为“y”)。在我自己的代码中,我使用命名约定.._makergenerate_.. 来表示我正在调用函数工厂。但这只是我个人的情况,而且该约定绝不是标准的,也不是在野外被广泛采用的。

因此,对于您的代码,您可以将其重构为:

exports.generateUrl = function (req, res) 
    var id = req.query.someParameter;

    var query = MyMongooseModel.findOne('id': id);
    query.exec(make_queryHandler(req,res));
;

function make_queryHandler (req, res) 
    return function (err, mongooseModel) 
        if(err) 
            //deal with it
        
        else if (!mongooseModel) 
            generateUrl(Id,make_urlGeneratorHandler(req,res));
         else 
            //deal with already exists
        


function make_urlGeneratorHandler (req, res) 
    return function (err, text, url) 
        if (err) 
            res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
            return;
        
        var newMongooseModel = new AnotherMongooseModel();
        newMongooseModel.id = id;
        newMongooseModel.save(make_modelSaveHandler(req,res));


function make_modelSaveHandler (req, res) 
    return function (err) 
        if (err) res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
        else res.send(url: url, text: text);

这会使嵌套的回调变平。作为一个额外的好处,您可以正确命名该函数应该做什么。我认为这是一种很好的做法。

它还有一个额外的优势,即它比使用匿名回调(使用嵌套回调或使用承诺)要快得多,但如果您将命名函数传递给 promise.then() 而不是匿名函数,那么您将获得相同的加速好处)。之前的一个 SO 问题(我的 google-fu 今天让我失望了)发现命名函数的速度是 node.js 中匿名函数的两倍以上(如果我没记错的话,它快了 5 倍以上)。

【讨论】:

虽然这个问题得到了很好的答案,但我选择了这个答案,因为我认为它会导致最干净的代码。变量的来源很明显,而且非常平坦。谢谢! 这看起来是避免回调地狱的好方法。有没有反对使用这个的论据,或者有什么缺点?【参考方案2】:

命名函数将在与匿名函数相同的范围内执行,并且可以访问您当前使用的所有变量。这种方法将使您的代码嵌套更少且更具可读性(这很好),但在技术上仍处于“回调地狱”中。避免这种情况的最好方法是用像Q 这样的promise 库来包装你的异步库(假设它们还没有提供promise)。 IMO,promise 提供了更清晰的执行路径图。

您可以通过使用bind 将参数绑定到您的命名函数来避免不知道变量来自何处的困境,例如:

function handleRequest(res, err, text, url) 
    if (err) 
        res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
        return;
    
    var newMongooseModel = new AnotherMongooseModel();
    newMongooseModel.id = id;

    newMongooseModel.save(function (err) 
        if (err) 
            res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
         else 
            res.send(url: url, text: text);
        
    );


...
generateUrl(Id, handleRequest.bind(null, res));

【讨论】:

我对命名函数的担忧是,即使它们可以访问相同的变量,但在读取这些变量来自何处的函数时并不清楚。【参考方案3】:

使用承诺。使用 Q 和 mongoose-q 它会给出:类似的东西:

exports.generateUrl = function (req, res) 
    var id = req.query.someParameter;
    var text = "";

    var query = MyMongooseModel.findOne('id': id);
    query.execQ().then(function (mongooseModel) 

        if (!mongooseModel) 
            return generateUrl(Id)

     ).then(function (text) 
       var newMongooseModel = new AnotherMongooseModel();
       newMongooseModel.id = id;
       text = text;

       newMongooseModel.saveQ()
     ).then(function (url) 
        res.send(url: url, text: text);
     ).fail(function(err) 
        res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
     );
;

【讨论】:

哦,如果我需要在 then 中进行另一个 Mongoose 查询,那么我仍然“卡住”了一个额外的函数嵌套。 (编辑:删除了关于函数之间级联变量的问题) 然后你必须在 generateUrl 范围内创建一个 var。使用 var text 查看更新的答案

以上是关于避免 nodeJs 中的回调地狱/将变量传递给内部函数的主要内容,如果未能解决你的问题,请参考以下文章

如何优雅的处理Nodejs中的异步回调

回调地狱,地理位置 - JavaScript

Nodejs 异步编程 - 为啥需要“异步”模块?啥是“回调地狱”/“末日金字塔”?

JavaScript 使用Async 和 Promise 完美解决回调地狱

ES6深入浅出-9 Promise 3 视频-1.回调与回调地狱

Node.JS:如何将变量传递给异步回调? [复制]