Comprendre les générateurs en JavaScript

L’auteur a sélectionné l’Open Internet/Free Speech Fund pour recevoir un don dans le cadre du programme Write for DOnations.

Introduction

En ECMAScript 2015, les générateurs ont été introduits dans le langage JavaScript. Un générateur est un processus qui peut être mis en pause et repris et qui peut donner plusieurs valeurs. Un générateur en JavaScript se compose d’une fonction de générateur, qui renvoie un objet itérable Generator.

Les générateurs peuvent maintenir l’état, fournissant un moyen efficace de faire des itérateurs, et sont capables de traiter des flux de données infinis, ce qui peut être utilisé pour mettre en œuvre un défilement infini sur le frontend d’une application web, pour opérer sur des données d’ondes sonores, et plus encore. En outre, lorsqu’ils sont utilisés avec des Promesses, les générateurs peuvent imiter la fonctionnalité async/await, ce qui nous permet de traiter le code asynchrone d’une manière plus directe et plus lisible. Bien que async/await soit un moyen plus répandu de traiter les cas d’utilisation asynchrones communs et simples, comme la récupération de données à partir d’une API, les générateurs ont des fonctionnalités plus avancées qui font qu’il vaut la peine d’apprendre à les utiliser.

Dans cet article, nous couvrirons comment créer des fonctions de générateur, comment itérer sur des objets Generator, la différence entre yield et return à l’intérieur d’un générateur, et d’autres aspects du travail avec les générateurs.

Fonctions de générateur

Une fonction de générateur est une fonction qui renvoie un objet Generator, et est définie par le mot-clé function suivi d’un astérisque (*), comme indiqué dans ce qui suit :

// Generator function declarationfunction* generatorFunction() {}

Occasionnellement, vous verrez l’astérisque à côté du nom de la fonction, par opposition au mot-clé de la fonction, comme function *generatorFunction(). Cela fonctionne de la même manière, mais function* est une syntaxe plus largement acceptée.

Les fonctions génératrices peuvent également être définies dans une expression, comme les fonctions régulières :

// Generator function expressionconst generatorFunction = function*() {}

Les générateurs peuvent même être les méthodes d’un objet ou d’une classe :

Les exemples tout au long de cet article utiliseront la syntaxe de déclaration des fonctions génératrices.

Note : Contrairement aux fonctions régulières, les générateurs ne peuvent pas être construits avec le mot-clé new, ni être utilisés en conjonction avec des fonctions flèches.

Maintenant que vous savez comment déclarer les fonctions génératrices, regardons les objets itérables Generator qu’elles retournent.

Objets générateurs

Traditionnellement, les fonctions en JavaScript s’exécutent jusqu’à la fin, et l’appel d’une fonction renvoie une valeur lorsqu’elle arrive au mot-clé return. Si le mot-clé return est omis, une fonction retournera implicitement undefined.

Dans le code suivant, par exemple, nous déclarons une fonction sum() qui renvoie une valeur qui est la somme de deux arguments entiers :

// A regular function that sums two valuesfunction sum(a, b) { return a + b}

L’appel de la fonction renvoie une valeur qui est la somme des arguments :

const value = sum(5, 6) // 11

Une fonction générateur, cependant, ne renvoie pas une valeur immédiatement, et renvoie plutôt un objet itérable Generator. Dans l’exemple suivant, nous déclarons une fonction et lui donnons une seule valeur de retour, comme une fonction standard:

// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}

Lorsque nous invoquons la fonction de générateur, elle retournera l’objet Generator, que nous pouvons affecter à une variable:

// Assign the Generator object to generatorconst generator = generatorFunction()

S’il s’agissait d’une fonction régulière, nous nous attendrions à ce que generator nous donne la chaîne de caractères retournée dans la fonction. Cependant, ce que nous obtenons en réalité est un objet dans un état suspended. L’appel de generator donnera donc une sortie similaire à ce qui suit :

L’objet Generator retourné par la fonction est un itérateur. Un itérateur est un objet qui a une méthode next() disponible, qui est utilisée pour itérer à travers une séquence de valeurs. La méthode next() renvoie un objet avec des propriétés value et done. value représentent la valeur retournée, et done indiquent si l’itérateur a parcouru toutes ses valeurs ou non.

Sachant cela, appelons next() sur notre generator et obtenons la valeur actuelle et l’état de l’itérateur :

// Call the next method on the Generator objectgenerator.next()

Cela donnera la sortie suivante :

Output
{value: "Hello, Generator!", done: true}

La valeur retournée par l’appel de next() est Hello, Generator!, et l’état de done est true, car cette valeur provient d’un return qui a fermé l’itérateur. Puisque l’itérateur est terminé, l’état de la fonction générateur passera de suspended à closed. Appeler generator à nouveau donnera ce qui suit :

Output
generatorFunction {<closed>}

À l’heure actuelle, nous avons seulement démontré comment une fonction de générateur peut être un moyen plus complexe d’obtenir la valeur return d’une fonction. Mais les fonctions génératrices ont également des caractéristiques uniques qui les distinguent des fonctions normales. Dans la prochaine section, nous apprendrons à connaître l’opérateur yield et nous verrons comment un générateur peut mettre en pause et reprendre l’exécution.

opérateurs de rendement

Les générateurs introduisent un nouveau mot-clé dans JavaScript : yield. yield peut mettre en pause une fonction de générateur et retourner la valeur qui suit yield, fournissant un moyen léger d’itérer à travers des valeurs.

Dans cet exemple, nous allons mettre en pause la fonction de générateur trois fois avec des valeurs différentes, et retourner une valeur à la fin. Ensuite, nous affecterons notre objet Generator à la variable generator.

Maintenant, lorsque nous appelons next() sur la fonction générateur, elle se mettra en pause chaque fois qu’elle rencontrera yield. done sera mis à false après chaque yield, indiquant que le générateur n’a pas terminé. Une fois qu’il rencontrera un return, ou qu’il n’y aura plus de yield rencontrés dans la fonction, done basculera à true, et le générateur sera terminé.

Utiliser la méthode next() quatre fois de suite :

// Call next four timesgenerator.next()generator.next()generator.next()generator.next()

Cela donnera les quatre lignes de sortie suivantes dans l’ordre :

Output
{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}

Notez qu’un générateur n’a pas besoin d’un return ; s’il est omis, la dernière itération retournera {value: undefined, done: true}, comme tous les appels ultérieurs à next() après qu’un générateur se soit terminé.

Itération sur un générateur

En utilisant la méthode next(), nous avons manuellement itéré sur l’objet Generator, recevant toutes les propriétés value et done de l’objet complet. Cependant, tout comme Array, Map et Set, un Generator suit le protocole d’itération, et peut être itéré avec for...of:

// Iterate over Generator objectfor (const value of generator) { console.log(value)}

Cela retournera ce qui suit:

Output
NeoMorpheusTrinity

L’opérateur d’étalement peut également être utilisé pour affecter les valeurs d’un Generator à un tableau.

// Create an array from the values of a Generator objectconst values = console.log(values)

Cela donnera le tableau suivant:

Output
(3)

L’étalement et for...of ne factoriseront pas le return dans les valeurs (dans ce cas, il aurait été 'The Oracle').

Note : Bien que ces deux méthodes soient efficaces pour travailler avec des générateurs finis, si un générateur traite un flux de données infini, il ne sera pas possible d’utiliser directement spread ou for...of sans créer une boucle infinie.

Fermeture d’un générateur

Comme nous l’avons vu, un générateur peut avoir sa propriété done définie à true et son statut défini à closed en itérant à travers toutes ses valeurs. Il existe deux façons supplémentaires d’annuler immédiatement un générateur : avec la méthode return(), et avec la méthode throw().

Avec return(), le générateur peut être terminé à n’importe quel moment, tout comme si une instruction return avait été dans le corps de la fonction. Vous pouvez passer un argument dans return(), ou le laisser vide pour une valeur indéfinie.

Pour démontrer return(), nous allons créer un générateur avec quelques valeurs yield mais sans return dans la définition de la fonction :

function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()

Le premier next() nous donnera 'Neo', avec done réglé sur false. Si nous invoquons une méthode return() sur l’objet Generator juste après, nous obtiendrons maintenant la valeur passée et done fixé à true. Tout appel supplémentaire à next() donnera la réponse du générateur complété par défaut avec une valeur indéfinie.

Pour démontrer cela, exécutez les trois méthodes suivantes sur generator:

generator.next()generator.return('There is no spoon!')generator.next()

Cela donnera les trois résultats suivants:

Output
{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}

La méthode return() a forcé l’objet Generator à compléter et à ignorer tout autre mot-clé yield. Ceci est particulièrement utile dans la programmation asynchrone lorsque vous devez rendre les fonctions annulables, comme l’interruption d’une requête web lorsqu’un utilisateur veut effectuer une action différente, car il n’est pas possible d’annuler directement une Promise.

Si le corps d’une fonction de générateur a un moyen d’attraper et de traiter les erreurs, vous pouvez utiliser la méthode throw() pour lancer une erreur dans le générateur. Cela démarre le générateur, lance l’erreur et termine le générateur.

Pour démontrer cela, nous allons mettre un try...catch à l’intérieur du corps de la fonction du générateur et enregistrer une erreur si on en trouve une:

Maintenant, nous allons exécuter la méthode next(), suivie de throw() :

generator.next()generator.throw(new Error('Agent Smith!'))

Cela donnera la sortie suivante:

Output
{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}

En utilisant throw(), nous avons injecté une erreur dans le générateur, qui a été capturée par le try...catch et enregistrée dans la console.

Méthodes et états de l’objet générateur

Le tableau suivant montre une liste de méthodes qui peuvent être utilisées sur les objets Generator :

Méthode Description
next() Retourne la prochaine valeur dans un générateur
return() Retourne une valeur dans un générateur et termine le générateur
throw() Lance une erreur et termine le générateur

Le tableau suivant liste les états possibles d’un objet Generator :

Status Description
suspended Le générateur a arrêté l’exécution mais ne s’est pas terminé
closed Le générateur s’est terminé soit en rencontrant une erreur, retournant, ou en itérant à travers toutes les valeurs

yield Delegation

En plus de l’opérateur régulier yield, les générateurs peuvent également utiliser l’expression yield* pour déléguer d’autres valeurs à un autre générateur. Lorsque le yield* est rencontré à l’intérieur d’un générateur, il ira à l’intérieur du générateur délégué et commencera à itérer à travers tous les yield jusqu’à ce que ce générateur soit fermé. Cela peut être utilisé pour séparer différentes fonctions de générateur pour organiser sémantiquement votre code, tout en ayant tous leurs yields itérables dans le bon ordre.

Pour démontrer, nous pouvons créer deux fonctions génératrices, dont l’une va yield*opérer sur l’autre :

Puis, itérons à travers la fonction génératrice begin() :

// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}

Cela donnera les valeurs suivantes dans l’ordre où elles sont générées:

Output
1234

Le générateur extérieur a donné les valeurs 1 et 2, puis a délégué à l’autre générateur avec yield*, qui a renvoyé 3 et 4.

yield* peut également déléguer à tout objet qui est itérable, comme un tableau ou une carte. La délégation de rendement peut être utile dans l’organisation du code, puisque toute fonction dans un générateur qui voulait utiliser yield devrait également être un générateur.

Flux de données infinis

Un des aspects utiles des générateurs est la capacité de travailler avec des flux de données et des collections infinis. Cela peut être démontré en créant une boucle infinie à l’intérieur d’une fonction de générateur qui incrémente un nombre par un.

Dans le bloc de code suivant, nous définissons cette fonction de générateur et nous lançons ensuite le générateur:

Maintenant, itérez à travers les valeurs en utilisant next():

// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()

Cela donnera la sortie suivante:

Output
{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}

La fonction renvoie des valeurs successives dans la boucle infinie tandis que la propriété done reste false, assurant qu’elle ne se terminera pas.

Avec les générateurs, vous n’avez pas à vous soucier de créer une boucle infinie, car vous pouvez arrêter et reprendre l’exécution à volonté. Cependant, vous devez toujours faire attention à la façon dont vous invoquez le générateur. Si vous utilisez spread ou for...of sur un flux de données infini, vous itérerez toujours sur une boucle infinie d’un seul coup, ce qui fera planter l’environnement.

Pour un exemple plus complexe de flux de données infini, nous pouvons créer une fonction de générateur Fibonacci. La séquence de Fibonacci, qui ajoute continuellement les deux valeurs précédentes ensemble, peut être écrite en utilisant une boucle infinie dans un générateur comme suit :

Pour tester cela, nous pouvons boucler sur un nombre fini et imprimer la séquence de Fibonacci sur la console.

// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}

Cela donnera ce qui suit :

Output
0112358132134

La capacité de travailler avec des ensembles de données infinis est une partie de ce qui rend les générateurs si puissants. Cela peut être utile pour des exemples comme l’implémentation du scroll infini sur le frontend d’une application web.

Passer des valeurs dans les générateurs

Tout au long de cet article, nous avons utilisé les générateurs comme itérateurs, et nous avons donné des valeurs à chaque itération. En plus de produire des valeurs, les générateurs peuvent également consommer des valeurs de next(). Dans ce cas, yield contiendra une valeur.

Il est important de noter que le premier next() qui est appelé ne passera pas de valeur, mais démarrera seulement le générateur. Pour le démontrer, nous pouvons enregistrer la valeur de yield et appeler next() plusieurs fois avec certaines valeurs.

Cela donnera la sortie suivante :

Output
100200{value: "The end", done: true}

Il est également possible d’ensemencer le générateur avec une valeur initiale. Dans l’exemple suivant, nous ferons une boucle for et passerons chaque valeur dans la méthode next(), mais passerons également un argument à la fonction initiale:

Nous récupérerons la valeur de next() et céderons une nouvelle valeur à l’itération suivante, qui est la valeur précédente fois dix. Cela donnera ce qui suit :

Output
010203040

Une autre façon de traiter le démarrage d’un générateur est d’envelopper le générateur dans une fonction qui appellera toujours next() une fois avant de faire quoi que ce soit d’autre.

async/await avec les générateurs

Une fonction asynchrone est un type de fonction disponible dans le JavaScript ES6+ qui rend le travail avec des données asynchrones plus facile à comprendre en les faisant apparaître synchrones. Les générateurs ont un éventail de capacités plus étendu que les fonctions asynchrones, mais sont capables de reproduire un comportement similaire. La mise en œuvre de la programmation asynchrone de cette manière peut augmenter la flexibilité de votre code.

Dans cette section, nous allons démontrer un exemple de reproduction de async/await avec des générateurs.

Construisons une fonction asynchrone qui utilise l’API Fetch pour obtenir des données de l’API JSONPlaceholder (qui fournit des données JSON d’exemple à des fins de test) et enregistre la réponse dans la console.

Débutez en définissant une fonction asynchrone appelée getUsers qui récupère les données de l’API et renvoie un tableau d’objets, puis appelez getUsers:

Cela donnera des données JSON similaires à ce qui suit:

En utilisant des générateurs, nous pouvons créer quelque chose de presque identique qui n’utilise pas les mots-clés async/await. Au lieu de cela, il utilisera une nouvelle fonction que nous créons et des valeurs yield au lieu des promesses await.

Dans le bloc de code suivant, nous définissons une fonction appelée getUsers qui utilise notre nouvelle fonction asyncAlt (que nous écrirons plus tard) pour imiter async/await.

Comme nous pouvons le voir, il semble presque identique à l’implémentation async/await, sauf qu’il y a une fonction de générateur qui est passée et qui donne des valeurs.

Maintenant, nous pouvons créer une fonction asyncAlt qui ressemble à une fonction asynchrone. asyncAlt a une fonction générateur comme paramètre, qui est notre fonction qui donne les promesses que fetch renvoie. asyncAlt renvoie une fonction elle-même, et résout chaque promesse qu’elle trouve jusqu’à la dernière:

Cela donnera la même sortie que la version async/await:

Notez que cette implémentation sert à démontrer comment les générateurs peuvent être utilisés à la place de async/await, et n’est pas une conception prête pour la production. Il n’a pas de gestion d’erreur mise en place, et n’a pas la possibilité de passer des paramètres dans les valeurs produites. Bien que cette méthode puisse ajouter de la flexibilité à votre code, souvent async/await sera un meilleur choix, car elle abstrait les détails de mise en œuvre et vous permet de vous concentrer sur l’écriture de code productif.

Conclusion

Les générateurs sont des processus qui peuvent arrêter et reprendre l’exécution. Ils constituent une fonctionnalité puissante et polyvalente de JavaScript, même s’ils ne sont pas couramment utilisés. Dans ce tutoriel, nous avons appris les fonctions de générateur et les objets de générateur, les méthodes disponibles pour les générateurs, les opérateurs yield et yield*, et les générateurs utilisés avec des ensembles de données finis et infinis. Nous avons également exploré une façon d’implémenter du code asynchrone sans callbacks imbriqués ou longues chaînes de promesses.

Si vous souhaitez en savoir plus sur la syntaxe JavaScript, jetez un œil à nos tutoriels Comprendre ceci, lier, appeler et appliquer en JavaScript et Comprendre les objets Map et Set en JavaScript.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.