使用 node.js 理解 javascript 回调的概念,尤其是在循环中

IT技术 javascript callback node.js
2021-01-14 17:05:05

我刚开始使用 node.js。我已经做了一些 ajax 的东西,但没有什么太复杂的,所以回调仍然有点超出我的头脑。我查看了 async,但我所需要的只是按顺序运行一些函数。

我基本上有一些东西可以从 API 中提取一些 JSON,创建一个新的,然后用它做一些事情。显然,我不能只运行它,因为它一次运行所有内容并且有一个空的 JSON。大多数进程必须按顺序运行,但是如果在从 API 中提取 JSON 时它可以在等待时提取其他 JSON,那就没问题了。将回调放入循环中时,我只是感到困惑。我如何处理索引?我想我已经看到一些地方在循环内使用回调作为一种递归函数并且根本不使用 for 循环。

简单的例子会有很大帮助。

1个回答

如果回调是在定义循环的同一范围内定义的(这种情况经常发生),则回调将可以访问索引变量。暂时抛开 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,因为回调是对其定义范围内的数据闭包(不要担心术语“闭包”,闭包并不复杂。)

我说最好不要在计算密集型生产代码中逐字执行上述操作的原因是代码在每次迭代都会创建一个函数(除非在编译器中进行花哨的优化,V8 非常聪明,但是优化创建这些函数是非平凡)。所以这里有一个稍微修改的例子:

var index;

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

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

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

现在,让我们一个反面的例子,一些工作:

foo();

function foo() {
    var index;

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

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

那失败了,因为myCallback没有在定义的同一范围(或嵌套范围)index中定义,因此indexmyCallback.

最后,让我们考虑在循环中设置事件处理程序,因为必须小心处理。在这里,我们将深入了解 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 使用更新的术语),但概念没有改变。

在(故障版本)函数(索引)中提供参数怎么样?那仍然是错误的还是不受欢迎的?
2021-03-18 17:05:05
令人难以置信的答案,以及你提到的优秀文章,对我帮助很大
2021-03-29 17:05:05
最后一个例子中的小错误: for (index = 0; index < processes.length; ++index) { 应该是: for (index = 0; index < commands.length; ++index) { :)
2021-04-04 17:05:05