【问题标题】:Pre-emptive: How to avoid deep callback hierarchy in Node? (Using Express) [duplicate]先发制人:如何避免 Node 中的深层回调层次结构? (使用快递)[重复]
【发布时间】:2026-01-18 12:55:01
【问题描述】:

我刚开始尝试使用 node(使用 Express 构建一个带有 MySql 数据库的简单网站)。

我基本上已经使用了 Express 提供的应用程序结构(就这个问题而言,这并不重要)。我有一个文件routes/index.js,它导出index 函数,每当对我的主页发出请求时,该函数就会被命中。 index.js的内容是:

var db = require('../db');                                                      

exports.index = function(req, res){                                             
    db.getConnection(function(err, connection) {                                
        connection.query('SELECT * FROM test_table', function (err, rows) {     
            var templateVariables = {                                           
                title: 'Index Page',                                            
                response: rows[0].text                                          
            };                                                                  
            res.render('index', templateVariables);                             
        });                                                                     
        connection.end();                                                                                     
    });                                                                                                       
};   

这显然是一个非常初步和轻量级的示例,但是在这个特定的索引页面GET 请求中,已经有一组深度为 3 的回调函数。每个 callbuck 必须存在于“父”的回调中,因为它取决于结果(在顺序执行的语言/环境中,这将是显而易见的和微不足道的)。

我的问题是,在构建更复杂且可能非常大的应用程序时,如何避免出现大量嵌套回调函数的问题?当您对逻辑有顺序依赖时,当然会出现这种情况。我知道 Node 的理念是异步的,但是当谈到等待来自数据库的数据并说我们正在运行 5 个单独的查询时,那会怎样呢?我们是否只需将单个多语句查询编写为原子单元?虽然这个问题并不是数据库独有的。

【问题讨论】:

    标签: javascript node.js callback express


    【解决方案1】:

    这里有一个关于这个问题的很好的一般性讨论: http://callbackhell.com/

    此外,许多人使用 Async 等模块来管理流控制问题。

    【讨论】:

    • 感谢您的链接 :)。
    【解决方案2】:

    由于您提到使用 Express,您可以使用 next() 作为回调的替代方法。

    app.get('/',function first(req,res,next){
       res.write('Hello ');
       res.locals.user = req.user;
       next();
       //Execute next handler in queue
    });
    
    app.get('/',function second(req,res,next){
       res.write('World!');
       //Use res.locals.user
       res.end();
    });
    
    //response shows Hello World!
    

    路由处理程序使用额外的参数next 并按照给定的顺序执行,直到其中一个返回响应。 next 要么根本不接受参数,要么接受错误作为参数。您可以在res.locals中设置要传递给下一个函数的变量

    【讨论】:

    • next() 的使用对于让多个处理程序处理相同(或相同类型)的请求(正如您所指出的)似乎是有意义的。尽管next() 方法提供的设施似乎不能解决任意分层回调的问题(就像我的数据库示例一样)。你会同意吗?不过感谢您的回复,很高兴知道。
    • 是的,序列化复杂的回调并不总是可行的。但是,如果您必须更改处理请求的步骤顺序,这将很有帮助。把它想象成分解成步骤的响应函数(易于调试,您可以重用/修改步骤函数)
    【解决方案3】:

    使用 PromiseFuture 库,例如 Q(在 npm 上可用)。

    引用 Q 的自述文件,promises 可以让你改变这个:

    step1(function (value1) {
        step2(value1, function(value2) {
            step3(value2, function(value3) {
                step4(value3, function(value4) {
                    // Do something with value4
                });
            });
        });
    });
    

    进入这个:

    Q.fcall(step1)
    .then(step2)
    .then(step3)
    .then(step4)
    .then(function (value4) {
        // Do something with value4
    }, function (error) {
        // Handle any error from step1 through step4
    })
    .done();
    

    我见过的所有其他回调地狱的解决方案都引入了似乎只是倒退了一步的权衡。异步操作形成较大操作之间的自然逻辑边界,因此如果您分解函数或沿这些边界进行模块化,您将获得微分解代码。

    【讨论】:

    • 人们可能会认为滥用/过度使用这样的库会变得很容易(Promise/Future)。您是否认为它有它的位置,但需要仔细检查何时使用它,何时不使用它?
    • @csjohn 只有在权衡的情况下才会滥用或过度使用。如果你更多地使用它,你甚至需要处理更少的回调地狱,这不是过度使用。但是,是的,您的所有代码都可能看起来像这样。然而,在 Q(我认为任何 promise 库)和回调之间转换很容易,所以你不会被这个决定卡住。
    【解决方案4】:

    我喜欢做的一种方式是这样......

    exports.index = function(req, res) {
      var connection
    
      db.getConnection(gotConnection)
    
      function gotConnection(err, _c) {
        connection = _c                                
        connection.query('SELECT * FROM test_table', gotData)
      }
    
      function gotData(err, rows) { 
        connection.end();        
        var templateVariables = {                                           
          title: 'Index Page', 
          response: rows[0].text     
        }
        res.render('index', templateVariables);
      }
    });
    

    您还应该始终处理代码中的错误,我假设您将它们排除在外是为了使代码在此处更易于阅读。

    【讨论】: