Entendendo o conceito de callbacks de javascript com node.js, especialmente em loops

Eu estou apenas começando com node.js. Eu fiz um pouco de coisas de ajax, mas nada muito complicado, então os retornos de chamada ainda são meio que sobre minha cabeça. Eu olhei async, mas tudo que eu preciso é executar algumas funções sequencialmente.

Eu basicamente tenho algo que puxa algum JSON de uma API, cria um novo e depois faz algo com isso. Obviamente, não posso simplesmente executá-lo porque ele roda tudo de uma vez e tem um JSON vazio. Principalmente, os processos precisam ser executados sequencialmente, mas se, ao puxar o JSON da API, ele puder extrair outro JSON enquanto aguarda, tudo bem. Eu fiquei confuso ao colocar o callback em um loop. O que eu faço com o índice? Eu acho que vi alguns lugares que usam callbacks dentro do loop como uma function recursiva e não usam loops de forma alguma.

Exemplos simples ajudariam muito.

Se o retorno de chamada estiver definido no mesmo escopo em que o loop está definido (o que é freqüentemente o caso), o retorno de chamada terá access à variável de índice. Deixando de lado os detalhes do NodeJS por um momento, vamos considerar esta function:

function doSomething(callback) { callback(); } 

Essa function aceita uma referência de function de retorno de chamada e tudo o que ela faz é chamá-la. Não é muito excitante. 🙂

Agora vamos usar isso em um loop:

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

(No código de computação intensiva - como um processo de servidor - melhor não fazer literalmente o código de produção acima, voltaremos a isso em um momento.)

Agora, quando executamos isso, vemos a saída esperada:

 index = 0 index = 1 index = 2 

Nosso retorno de chamada pôde acessar o index , porque o retorno de chamada é um fechamento sobre os dados no escopo onde ele está definido. (Não se preocupe com o termo "fechamento", os fechamentos não são complicados .)

A razão pela qual eu disse que provavelmente é melhor não fazer literalmente o código de produção com uso intensivo de computação é que o código cria uma function em cada iteração (exceto otimização no compilador, e o V8 é muito inteligente, mas otimizar a criação dessas funções é não trivial). Então aqui está um exemplo ligeiramente reformulado:

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

Isso pode parecer um pouco surpreendente, mas ainda funciona da mesma maneira, e ainda tem a mesma saída, porque doSomethingCallback ainda é um fechamento sobre o index , então ele ainda vê o valor do index partir de quando é chamado. Mas agora há apenas uma function doSomethingCallback , em vez de uma nova em cada loop.

Agora vamos dar um exemplo negativo, algo que não funciona:

 foo(); function foo() { var index; for (index = 0; index < 3; ++index) { doSomething(myCallback); } } function myCallback() { console.log("index = " + index); // <== Error } 

Isso falha, porque myCallback não está definido no mesmo escopo (ou em um escopo nested) em que o index está definido e, portanto, o index é indefinido em myCallback .

Finalmente, vamos considerar a configuração de manipuladores de events em um loop, porque é preciso ter cuidado com isso. Aqui vamos mergulhar um pouco no 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 }); } 

Parece que o acima deve funcionar da mesma forma que os nossos loops anteriores, mas há uma diferença crucial. Em nossos loops anteriores, o retorno de chamada estava sendo chamado imediatamente e, assim, ele viu o valor do index correto porque o index não tinha tido a chance de continuar ainda. No entanto, acima, vamos girar o loop antes que o retorno de chamada seja chamado. O resultado? Nós vemos

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

Esse é um ponto crucial. Um fechamento não tem uma cópia dos dados que ele fecha, ele tem uma referência ao vivo para ele. Assim, no momento em que o callback de exit em cada um desses processos é executado, o loop já estará completo, para que todas as três chamadas vejam o mesmo valor de index (seu valor no final do loop).

Podemos consertar isso fazendo com que o callback use uma variável diferente que não vai mudar, assim:

 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"); }; } 

Agora nós emitimos os valores corretos (em qualquer ordem que os processos saiam):

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

A maneira como isso funciona é que o retorno de chamada que atribuímos ao evento de exit fecha sobre o argumento i na chamada que fazemos para makeExitCallback . O primeiro retorno de chamada que makeExitCallback cria e retorna se fecha sobre o valor i dessa chamada para makeExitCallback , o segundo retorno de chamada que ele cria se fecha sobre o valor i dessa chamada para makeExitCallback (que é diferente do valor i para a chamada anterior) etc.

Se você der o artigo ligado acima de uma leitura, um número de coisas deve ser mais claro. A terminologia do artigo é um pouco datada (o ECMAScript 5 usa uma terminologia atualizada), mas os conceitos não mudaram.