【问题标题】:Avoiding callback hell in nodeJs / Passing variables to inner functions避免 nodeJs 中的回调地狱/将变量传递给内部函数
【发布时间】: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

【问题讨论】:

    标签: javascript node.js express callback mongoose


    【解决方案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 查看更新的答案
    猜你喜欢
    • 2017-08-18
    • 2017-05-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多