一尘不染

了解使用node.js进行JavaScript回调的概念,尤其是在循环中

node.js

我只是从node.js开始。我做了一些ajax的工作,但是没有什么太复杂的,所以回调仍然有点麻烦。我看了看异步,但我所需要的只是顺序运行一些功能。

我基本上有一些东西可以从API中提取一些JSON,然后创建一个新的JSON,然后对此进行处理。显然,我不能只运行它,因为它可以一次运行所有内容并且具有空的JSON。通常,流程必须按顺序运行,但是如果在从API中提取JSON时可以在等待时提取其他JSON,那很好。将回调置于循环中时,我只是感到困惑。我该如何处理索引?我想我已经看到一些地方在循环中使用回调作为递归函数,而根本不使用循环。

简单的例子会很有帮助。


阅读 214

收藏
2020-07-07

共1个答案

一尘不染

如果在与循环定义相同的作用域中定义了回调(通常是这种情况),则回调将有权访问index变量。暂时搁置NodeJS的细节,让我们考虑一下这个函数:

function doSomething(callback) {
    callback();
}

该函数接受回调函数引用,而它所做的就是调用它。不太令人兴奋。:-)

现在让我们在循环中使用它:

var index;

for (index = 0; index < 3; ++index) {
    doSomething(function() {
        console.log("index = " + index);
    });
}

(在诸如服务器进程之类的计算密集型代码中,最好不要在生产代码中按字面意思进行上述操作,我们稍后再讲。)

现在,当我们运行它时,我们将看到预期的输出:

index = 0
index = 1
index = 2

我们的回调能够访问index,因为回调是对定义了作用域的数据的 封闭 。(不要担心术语“
closure”,闭包并不复杂。)

我说过最好不要在计算密集型生产代码中执行上述操作,原因是该代码会在 每次迭代
创建一个函数(除非在编译器中进行了花哨的优化,而V8非常聪明,但通过创建这些函数来进行优化是可以的)不平凡的)。因此,这是一个稍作修改的示例:

var index;

for (index = 0; index < 3; ++index) {
    doSomething(doSomethingCallback);
}

function doSomethingCallback() {
    console.log("index = " + index);
}

这可能看起来有点令人惊讶,但是它仍然以相同的方式工作,并且仍然具有相同的输出,因为doSomethingCallback它仍然是over的闭包index,因此index在调用时仍能看到as
的值。但是现在只有一个doSomethingCallback函数,而不是每个循环都有一个新函数。

现在让我们举一个负面的例子,那 行不通的:

foo();

function foo() {
    var index;

    for (index = 0; index < 3; ++index) {
        doSomething(myCallback);
    }
}

function myCallback() {
    console.log("index = " + index); // <== Error
}

之所以失败,是因为myCallback未在中定义相同的范围(或嵌套范围)中index进行定义,因此index在中未定义myCallback

最后,让我们考虑在一个循环中设置事件处理程序,因为对此必须要小心。在这里,我们将深入探讨NodeJS:

var spawn = require('child_process').spawn;

var commands = [
    {cmd: 'ls', args: ['-lh', '/etc' ]},
    {cmd: 'ls', args: ['-lh', '/usr' ]},
    {cmd: 'ls', args: ['-lh', '/home']}
];
var index, command, child;

for (index = 0; index < commands.length; ++index) {
    command = commands[index];
    child = spawn(command.cmd, command.args);
    child.on('exit', function() {
        console.log("Process index " + index + " exited"); // <== WRONG
    });
}

看起来
像上面应该工作相同的方式,我们前面的循环一样,但有一个关键的区别。在我们之前的循环中,回调被立即调用,因此它看到了正确的index值,因为index还没有机会继续前进。不过,在上面的代码中,我们将在调用回调之前旋转整个循环。结果?我们看

Process index 3 exited
Process index 3 exited
Process index 3 exited

这是关键点。闭包没有其关闭的数据的 副本 ,它具有对其的 实时引用
。因此,当exit运行这些进程中的每个进程的回调时,循环将已经完成,因此所有三个调用都将看到相同的index值(其值在循环 结束 时)。

我们可以通过使回调使用不会改变的 其他 变量来解决此问题,如下所示:

var spawn = require('child_process').spawn;

var commands = [
    {cmd: 'ls', args: ['-lh', '/etc' ]},
    {cmd: 'ls', args: ['-lh', '/usr' ]},
    {cmd: 'ls', args: ['-lh', '/home']}
];
var index, command, child;

for (index = 0; index < commands.length; ++index) {
    command = commands[index];
    child = spawn(command.cmd, command.args);
    child.on('exit', makeExitCallback(index));
}

function makeExitCallback(i) {
    return function() {
        console.log("Process index " + i + " exited");
    };
}

现在,我们输出正确的值(以进程退出的顺序):

Process index 1 exited
Process index 2 exited
Process index 0 exited

起作用的方式是,我们分配给exit事件的回调i在对的调用中的参数上关闭makeExitCallback。第一回调makeExitCallback创建并返回关闭在i该呼叫到值makeExitCallback,它在创建关闭第二回调i为值
呼叫makeExitCallback(这是比不同i的先前调用值)等。

如果您对上面链接的文章进行了阅读,那么很多事情都应该更加清楚。本文中的术语有些过时(ECMAScript 5使用更新的术语),但是概念没有改变。

2020-07-07