Node.js Generators et comparer avec les rappels

⚡ Résumé intelligent

GeneratorDans Node.js, les fonctions `yield` sont des fonctions dont l'exécution peut être suspendue et reprise à la demande. Le mot-clé `yield` met la fonction en pause, la méthode `next` la reprend, et cette association permet de simplifier des fonctions de rappel profondément imbriquées en un code séquentiel lisible.

  • 🇧🇷 Comportement fondamental : Un générateur se suspend à chaque cycle et reprend exactement là où il s'était arrêté lorsque le cycle suivant est appelé.
  • 🇧🇷 Déclaration: Un astérisque après le mot-clé de la fonction indique qu'il s'agit d'une fonction génératrice et non d'une fonction ordinaire.
  • (I.e. Canal bidirectionnel : La valeur passée à next devient le résultat de l'expression yield qui a mis la fonction en pause.
  • 🪆 L'enfer des rappels : Les fonctions de rappel imbriquées deviennent illisibles à mesure que des étapes sont ajoutées, problème que les générateurs ont été adoptés pour résoudre.
  • 🏃 Coureur requis : Un générateur ne s'exécute pas de lui-même ; une fonction d'exécution doit appeler la fonction « next » de manière répétée pour que le travail asynchrone puisse se poursuivre.
  • 🦥 Exécution paresseuse : La suspension et la reprise permettent à un programme de récupérer des valeurs uniquement au moment où elles sont nécessaires.

Node.js Generators Comparé aux rappels

Dans cette présentation, nous allons examiner les générateurs et en quoi ils diffèrent des fonctions de rappel.

Que sont les générateurs ?

Generators est devenu populaire en Node.js en raison de ce dont ils sont capables.

  • GeneratorLes s sont des exécutions de fonctions qui peuvent être suspendues et reprises ultérieurement.
  • GeneratorLes fonctions de suspension sont utiles pour des concepts tels que « l'exécution paresseuse ». En suspendant l'exécution et en la reprenant à volonté, les valeurs ne sont extraites que lorsqu'elles sont nécessaires.

Generators ont les deux méthodes principales ci-dessous.

  1. Méthode de rendement — appelée à l'intérieur d'une fonction pour interrompre l'exécution à la ligne exacte où yield apparaît.
  2. Méthode suivante — appelée depuis l'application principale pour reprendre une fonction qui a cédé la main. L'exécution se poursuit jusqu'à la prochaine yield, ou jusqu'à la fin de la fonction.

Prenons l'exemple d'une fonction d'addition simple qui additionne deux nombres, en interrompant l'exécution à différents moments pour illustrer le comportement des générateurs.

Node.js Generators

function* Add(x) {
    yield x + 1;
    // The value passed into the second next() call lands here
    var y = yield(null);
    return x + y;
}

var gen = Add(5);

gen.next();      // runs to the first yield, returns { value: 6, done: false }
gen.next(6);     // resumes, y becomes 6, returns { value: 11, done: true }

Code Explication

  1. La première étape consiste à définir la fonction génératrice. Notez le * ajoutée à la function mot-clé. La fonction est appelée Add et prend un paramètre x.
  2. Le yield Ce mot-clé est spécifique aux générateurs et interrompt l'exécution de la fonction. L'exécution s'arrête ici jusqu'à ce que… next() est invoquée.
  3. La deuxième yield La fonction est à la fois mise en pause et reçoit une valeur : celle qui est passée à la fonction suivante. next() l'appel devient la valeur de y.
  4. appel gen.next() La première exécution rattrape la fonction jusqu'au premier yield. Appel gen.next(6) reprend le projet, attribue 6 à y, et retourne x + y.

⚠️ Correction : la liste originale contenait la ligne y = 6 juste après var y = yield(null);Cette affectation écrase toute valeur existante. next() fournitures, ce qui contredit l'objectif de l'exemple et rend le second yield Sans signification. La valeur est maintenant transmise par gen.next(6), c'est ainsi qu'un générateur reçoit réellement des données de l'appelant.

Rappels vs générateurs

Generators résout le problème connu sous le nom de « callback hell ». Les fonctions de rappel peuvent parfois devenir si profondément imbriquées lors du développement d'une application Node.js qu'elles sont difficiles à suivre.

C’est là que les générateurs s’avèrent utiles. Un exemple courant est la création de fonctions de temporisation. L’exemple ci-dessous construit une fonction de temporisation simple, puis l’appelle avec des délais de 1000, 2000 et 3000 ms.

Étape 1) Définissez la fonction de rappel avec le code de délai nécessaire.

Rappels vs. Generators

function Timedelay(ptime, callback) {

    setTimeout(function() {

        callback("Pausing for " + ptime);

    }, ptime);   // the delay must use ptime, the parameter name
}

Code Explication

  1. Une fonction appelée Timedelay est créé avec un paramètre ptime, qui indique le nombre de millisecondes de pause.
  2. setTimeout attend ce nombre de millisecondes, puis invoque la fonction de rappel fournie.

⚠️ Correction : Le code original a été validé. time comme deuxième argument à setTimeout, mais aucune variable nommée time existe — le paramètre est ptimeTel qu'il est écrit, il lance ReferenceError: time is not defined.

Étape 2) Considérons maintenant le code utilisant des fonctions de rappel. Pour chaîner des délais de 1000, 2000 et 3000 millisecondes, les fonctions de rappel sont imbriquées les unes dans les autres.

Rappels vs. Generators

Timedelay(1000, function(message) {

    console.log(message);

    Timedelay(2000, function(message) {

        console.log(message);

        Timedelay(3000, function(message) {

            console.log(message);
        });
    });
});

Code Explication

  1. Timedelay est appelée en tant que fonction de rappel avec la valeur 1000.
  2. À l'intérieur de cette fonction de rappel, Timedelay est rappelé avec 2000.
  3. À l'intérieur de celui-là, Timedelay est appelé une troisième fois avec 3000.

⚠️ Correction : chaque rappel dans le journal d'origine msg, mais le paramètre est nommé messageLes trois lignes ont lancé ReferenceError: msg is not defined.

Comme vous pouvez le constater dans le code ci-dessus, la situation se complique à chaque appel supplémentaire — cette imbrication est ce que décrit le « callback hell ».

Étape 3) Voyez maintenant le même comportement exprimé avec des générateurs. La séquence se lit comme trois lignes plates plutôt que comme trois niveaux d'imbrication.

Rappels vs. Generators

function* Messages() {
    console.log(yield Timedelay(1000));
    console.log(yield Timedelay(2000));
    console.log(yield Timedelay(3000));
}

Code Explication

  1. Une fonction génératrice est définie pour appeler la Timedelay la fonction.
  2. yield s'utilise avec Timedelay et 1000 comme valeur du paramètre.
  3. yield est utilisé à nouveau avec 2000, et une fois de plus avec 3000.

⚠️ Correction : l'original a écrit console,log(...) avec une virgule au lieu d'un point sur les trois lignes, ce qui constitue une erreur de syntaxe et empêche l'analyse du fichier.

Comment exécuter un Generator avec une fonction Runner

Le générateur ci-dessus est très clair, mais il ne fonctionnera pas de lui-même. C'est un détail que la plupart des explications omettent, et c'est pourquoi la version simplifiée peut paraître magique. Un générateur est une machine en pause ; il faut donc que quelque chose appuie constamment sur le bouton. Messages() Cette fonction crée simplement l'objet générateur et retourne immédiatement ; aucun minuteur ne démarre et rien n'est affiché.

L'élément manquant est un exécuteur : une petite fonction qui appelle next(), attend que le travail asynchrone soit terminé, puis appelle next() et voici le résultat.

// Timedelay now hands its result back to the runner
function Timedelay(ptime) {
    return function(resume) {
        setTimeout(function() {
            resume("Pausing for " + ptime);
        }, ptime);
    };
}

// The runner drives the generator to completion
function run(generatorFunction) {
    var it = generatorFunction();

    function step(value) {
        var result = it.next(value);
        if (result.done) return;
        // result.value is the function returned by Timedelay
        result.value(step);
    }

    step();
}

run(Messages);

Trois points permettent à cela de fonctionner. Premièrement, Timedelay Elle ne prend plus directement de fonction de rappel ; elle renvoie une fonction que l’exécuteur peut invoquer, lui rendant le contrôle lorsque le minuteur se déclenche. Deuxièmement, it.next(value) réinjecte le résultat précédent dans le générateur en pause, ce qui permet yield figurer à droite d'une tâche. Troisièmement, le coureur s'arrête dès que result.done La condition devient vraie, la récursion s'arrête donc naturellement. Ce modèle de fonctionnement est précisément ce que proposent des bibliothèques telles que co, et c'est le mécanisme qui async/await plus tard, absorbée par la langue elle-même.

Generators vs async/await : lequel utiliser ?

GeneratorLes s étaient la méthode standard pour écrire du code asynchrone plat avant async/await Les deux existent et partagent la même idée sous-jacente : interrompre l’exécution d’une fonction en cours. Node prend en charge cette fonctionnalité. async/await nativement depuis la version 7.6, ce qui modifie la recommandation pour toute personne commençant un nouveau travail aujourd'hui.

Aspect Generators asynchroniser / attendre
Coursier recherché Oui, écrit par vous ou par une bibliothèque Non, c'est inhérent à la langue.
Mot-clé de pause yield await
La gestion des erreurs Le coureur doit transmettre les erreurs Essai ordinaire et attraper
Peut s'interrompre en cours de route Oui, et la reprise peut être sélective. Non, il court jusqu'à la fin.
Meilleur pour Séquences paresseuses et flux infinis Travail asynchrone ordinaire

Utilisez le async/await Pour un flux asynchrone dans le nouveau code, il est préférable de supprimer le gestionnaire d'événements et de rétablir le mécanisme classique try/catch. Réservez les générateurs à leurs seules applications : production différée de valeurs, itération sur de longues séquences ou des séquences infinies, et tout cas où le consommateur détermine précisément le moment du calcul de la valeur suivante.

Commun Generator Erreurs et comment les corriger

La plupart des problèmes de générateurs proviennent de l'oubli que la fonction est inactive jusqu'à ce qu'un appel soit effectué. next()ou de petites erreurs de syntaxe autour de l'astérisque. Chaque symptôme ci-dessous indique sa cause et sa solution.

  • Rien ne se passe lorsque le générateur est appelé : L'appel renvoie un itérateur, et non un résultat. Appeler next(), ou passez-le à un coureur.
  • Erreur de syntaxe lors de l'exécution de yield : L'astérisque est manquant. yield n'est valable qu'à l'intérieur d'un function*.
  • La valeur renvoyée par next() est indéfinie : le premier next() L'appel ne peut pas fournir de valeur, car le générateur n'a pas atteint de valeur. yield Pour l'instant. Envoyez les données à partir du deuxième appel.
  • Boucle infinie dans un jeu de course : result.done n'est jamais vérifié, donc next() L'appel se poursuit au-delà de la fin. La fonction « Toujours retourner lorsque terminé » est vraie.
  • Les erreurs disparaissent silencieusement : Un rejet survenant lors de l'étape asynchrone n'est jamais renvoyé au générateur. Transmettez-le avec it.throw(err) afin qu'un bloc try-catch écrit à l'intérieur du générateur puisse effectivement le voir et le gérer.

FAQ

Un objet possédant deux propriétés : value, conservant ce qui avait été donné, et done, une variable booléenne qui passe à vrai une fois le générateur terminé. Les coureurs effectuent ce test pour savoir quand s'arrêter.

Oui. Un générateur contenant une boucle infinie est sûr car aucun calcul n'est effectué tant que la fonction `next()` n'est pas appelée. Cela rend les générateurs particulièrement adaptés aux flux d'identifiants ou aux résultats paginés.

Oui. AI Les outils remplacent yield par wait et abandonnent complètement le runner. RevExaminez attentivement les séquences paresseuses, car un générateur qui ne se termine jamais n'a pas d'équivalent direct à async await.

Assistants IA tracIl s'agit de déterminer si le processus a cessé d'appeler la fonction suivante, si une fonction de rappel de reprise n'a jamais été invoquée ou si la fonction « terminé » n'a jamais été atteinte. Ces trois cas expliquent la quasi-totalité des générateurs bloqués.

Non. Un générateur asynchrone est déclaré. async function* et est consommé avec for await...ofIl combine ces deux idées et convient au traitement de données asynchrones en flux continu, comme les curseurs de base de données.

Résumez cet article avec :