Javascript Node.js回调函数变量作用域问题

3

我正在重新学习JavaScript,在上周为大学作业编写此代码时,我认为可能有更好的执行方式。

app.get('/member/all', function(req, res) {    
    connection.query('CALL GetAllMembers()', function(err,rows){
        connection.query('CALL CountMembers()', function(err, allMembers){
            console.log(err);
            connection.query('CALL CountAllIndMembers()', function(err,indMembers){
                console.log(err);
                connection.query('CALL CountInactiveMembers()', function(err,inactiveMembers){
                    console.log(err);
                    connection.query('CALL CountAllMembersInGroups()', function(err,groupMembers){
                        console.log(err);
                        res.render('members', {members : rows[0], title : "All Members",  groupMembers : groupMembers[0][0].AllGrpMembers,
                            inactiveMembers : inactiveMembers[0][0].AllInactiveMembers, indMembers : indMembers[0][0].AllIndMembers,
                            allMembers : allMembers[0][0].AllMembers, statistics : true});
                        });
                    });
                });
            });
        });
    });
});

当我试图在app.get下声明变量,例如var allMembers...当回调被执行时,我无法设置allMembers = rowsFromTheCallback。似乎这是回调函数的本地变量。我确信这与变量作用域和/或提升有关。只是想问一下,是否有更好的方法来做到这一点,因为即使这个函数有效,它看起来非常丑陋呢!
提前谢谢。
杰克

将它们全部变成函数,而不是匿名调用它们! - Callum Linington
代码的丑陋是 Node 回调的不幸副作用。我建议看一下涉及生成器的东西(使用 yield 关键字而不是回调),但不知道是否稳定到足以在 Node 中使用。如果您想要更传统的东西,可以查看像 async.js 这样的库或使用 Promises 的东西。 - hugomg
阅读有关“延续传递风格”的资料将对您的理解大有裨益。 - Jason Aller
3个回答

1
就作用域而言,所有内部函数都应该能够读取和写入外部变量,除非被内部变量声明或函数参数遮蔽。
你遇到的问题可能与代码的异步性有关。请查看以下代码:
function delay(n, cb){
    setTimeout(function(){ bs(delay) }, delay);
}

function main(){
    var allMembers = 17;
    delay(500, function(){
        console.log(allMembers);  // This looks at the outer "allMembers"
        allMembers = 18;

        delay(200, function(allMembers){  // <-- SHADOW
            console.log(allMembers); // This looks at the allMembers from "delay 200"'s callback
            allMembers = 42;
        });

        delay(300, function(){
            console.log(allMembers); //This is the outside "allMembers" again
        });
    });

    return allMembers; // Still 17!
}

main();

main函数将在setTimeout函数执行之前就返回,因此它将返回该变量的原始值。为了等待内部回调函数运行,唯一的方法是使main函数接受一个回调函数来标记何时完成,而不仅仅是返回结果。

function main(onResult){
   delay(500, function(){
      //...
      onResult(allMembers);
   });

   // <-- no return value
});

main(function(allM){
    console.log(allM);
}); 

谢谢,这让我明白了 :) - user3621357
1
当需要对数据库进行一系列调用,并且每个后续调用都依赖于前一个调用的返回时,采用这种方法会导致性能不佳。更糟糕的是,如果选择的延迟时间不够长,它将失败。增加延迟只会使性能更差。 - Jason Aller
@JasonAller:我只是想把延迟函数作为一些简单的异步函数,供您在没有数据库的情况下进行测试和演示变量作用域。是的,我不建议使用实际的延迟来等待数据库的响应(事实上,嵌套的数据库调用通常是有问题的——是否有一种方法可以通过一个查询获取相同的数据总值得探究)。 - hugomg
抱歉,我在第一次阅读时错过了你的一些回答。当我阅读你的答案时,我正在专注于问题的另一个方面。 - Jason Aller

1

请查看异步库: https://github.com/caolan/async

async.series([
  getAllMembers,
  countMembers,
  ...
], function(err, results) {   
  // err contains an error if any of the functions fails. No more functions will be run.
  // results is an array containing results of each function if all the functions executed without errors
}));

function getAllMembers(callback) {
  connection.query('CALL CountMembers()', callback);
}

function countMembers(callback) {
 ...
}

如果函数的执行顺序不重要,可以使用async.parallel代替async.series。

0

使用库来处理和封装与异步调用的 "Continuation Passing Style" (CPS) 交互,这样做是有优势的。下面的代码不是来自库,但我将通过它并将其作为实现 CPS 的一种方式的示例。

设置适当队列的范围是第一步。此示例使用了最简单的方法:

var nextList = [];

接下来,我们需要一个处理第一个情况的方法,即需要将任务排队以在未来执行。在这种情况下,我专注于按顺序执行它们,因此我将其命名为next

function next() {
    var todo,
        current,
        task,
        args = {};
    if (arguments.length > 0) { // if called with parameters process them
        // if parameters aren't in an array wrap them
        if (!Array.isArray(arguments['0'])) {
            todo = [arguments];
        } else { // we were passed an array
            todo = [];
            arguments['0'].forEach(function (item) {
                // for each item we were passed add it to todo
                todo.push(item);
            });
        }
        nextList = todo.concat(nextList);
        // append the new items to the end of our list
    }
    if (nextList.length > 0) { // if there are still things to do
        current = Array.prototype.slice.apply(nextList.shift());
        task = current[0];
        args = current.slice(1);
        task.apply(null, args); // execute the next item in the list
    }
}

这使我们能够进行如下的调用:

.map(function (filepath) {
    tasks.push(
        [
            handleAsset,
            {
                'path': filepath,
            }
        ]
    );
});
tasks.push([done]);
next(tasks);

这将按顺序为每个文件调用异步的handleAsset函数。这将允许您将代码中的每个嵌套调用更改为单独的函数形式:

function memberAll() {
    app.get('/member/all', function(req, res) {
        if (err) {
            handleError(err, 'memberAll');
        } else {
            next(getAllMembers, 'parameters to that call if needed');
        }
    });
}

handleError是一个常见的错误处理程序,下一次调用允许您传递相关参数到下一个需要的函数。重要的是,在if语句的成功方面,您可以选择:

  • 有条件地调用几个函数中的一个
  • 使用要调用的调用数组调用下一个函数,例如,如果您有processFolderprocessFile函数,则可以期望处理文件夹可能涉及处理其他文件夹和文件,并且数量会有所不同
  • 什么也不做,除了使用没有参数调用next()并结束当前分支

在此基础上,还可以添加清空nextList的清洁函数,向nextList添加项目而无需从列表中调用项目等。此时的替代方法是使用现有库或继续编写自己的库。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接