31 votes

comprendre le concept des callbacks javascript avec node.js, notamment dans les boucles

Je commence tout juste à utiliser node.js. J'ai déjà fait un peu d'ajax, mais rien de très compliqué, donc les callbacks me dépassent encore un peu. J'ai regardé l'asynchronisme, mais tout ce dont j'ai besoin est d'exécuter quelques fonctions séquentiellement.

En gros, j'ai quelque chose qui tire du JSON d'une API, en crée un nouveau et en fait quelque chose. Évidemment, je ne peux pas simplement l'exécuter car il exécute tout en même temps et a un JSON vide. La plupart des processus doivent être exécutés séquentiellement, mais si pendant qu'il extrait le JSON de l'API, il peut extraire d'autres JSON pendant qu'il attend, alors c'est parfait. Je me suis juste embrouillé en plaçant le callback dans une boucle. Que dois-je faire avec l'index ? Je pense avoir vu certains endroits qui utilisent les callbacks à l'intérieur de la boucle comme une sorte de fonction récursive et n'utilisent pas du tout de boucles for.

Des exemples simples seraient d'une grande aide.

86voto

T.J. Crowder Points 285826

Si le callback est défini dans la même portée que la boucle (ce qui est souvent le cas), alors le callback aura accès à la variable index. Laissant de côté les particularités de NodeJS pour un moment, considérons cette fonction :

function doSomething(callback) {
    callback();
}

Cette fonction accepte une référence de fonction de rappel et tout ce qu'elle fait, c'est l'appeler. Pas très excitant :-)

Maintenant, utilisons ça dans une boucle :

var index;

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

(Dans le code à forte intensité de calcul - comme un processus serveur - il vaut mieux ne pas faire littéralement ce qui précède dans le code de production, nous y reviendrons dans un moment).

Maintenant, quand on exécute ça, on voit le résultat attendu :

index = 0
index = 1
index = 2

Notre callback a pu accéder à index car le callback est un fermeture sur les données dans la portée où il est défini. (Ne vous inquiétez pas du terme "fermeture". les fermetures ne sont pas compliquées .)

La raison pour laquelle j'ai dit qu'il est probablement préférable de ne pas faire littéralement ce qui précède dans le code de production à forte intensité de calcul est que le code crée une fonction sur le fichier chaque itération (sauf optimisation fantaisiste dans le compilateur, et V8 est très intelligent, mais l'optimisation de la création de ces fonctions n'est pas triviale). Voici donc un exemple légèrement retravaillé :

var index;

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

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

Cela peut paraître un peu surprenant, mais cela fonctionne toujours de la même manière, et a toujours le même résultat, car doSomethingCallback est toujours une fermeture sur index donc il voit toujours la valeur de index à partir du moment où il est appelé. Mais maintenant, il n'y a qu'un seul doSomethingCallback plutôt que d'en créer une nouvelle à chaque boucle.

Maintenant, prenons un exemple négatif, quelque chose qui n'a pas travail :

foo();

function foo() {
    var index;

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

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

Cela échoue, car myCallback n'est pas défini dans la même portée (ou une portée imbriquée) que index est défini dans, et donc index est indéfinie dans myCallback .

Enfin, examinons la mise en place de gestionnaires d'événements dans une boucle, car il faut faire attention à cela. Ici, nous allons nous plonger un peu dans 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
    });
}

Il semble comme ci-dessus devrait fonctionner de la même manière que nos boucles précédentes, mais il y a une différence cruciale. Dans nos boucles précédentes, le callback était appelé immédiatement, et donc il voyait la bonne valeur de index parce que index n'avait pas encore eu l'occasion de passer à autre chose. Dans l'exemple ci-dessus, cependant, nous allons faire tourner la boucle avant que le callback ne soit appelé. Le résultat ? Nous voyons

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

Il s'agit d'un point crucial. Une fermeture n'a pas de copie des données qu'il referme, il a une référence en direct à elle. Donc au moment où le exit sur chacun de ces processus est exécuté, la boucle sera déjà terminée, de sorte que les trois appels verront le même résultat. index (sa valeur à partir du fin de la boucle).

Nous pouvons corriger ce problème en faisant en sorte que le callback utilise un fichier différents variable qui ne changera pas, comme ceci :

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

Maintenant nous sortons les valeurs correctes (dans l'ordre où les processus sortent) :

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

La façon dont cela fonctionne est que le callback que nous assignons à la fonction exit se ferme sur l'événement i dans l'appel que nous faisons à makeExitCallback . Le premier callback qui makeExitCallback crée et renvoie des fermetures sur le i pour cet appel à makeExitCallback le deuxième callback qu'il crée ferme le dossier de l'utilisateur. i valeur pour que Appel à makeExitCallback (qui est différente de la i pour l'appel précédent), etc.

Si vous donnez l'article lié ci-dessus Après lecture, un certain nombre de choses devraient être plus claires. La terminologie de l'article est un peu dépassée (ECMAScript 5 utilise une terminologie actualisée), mais les concepts n'ont pas changé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X