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 :
OutputgeneratorFunction {<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:
OutputNeoMorpheusTrinity
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 yield
s 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:
Output1234
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 :
Output0112358132134
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 :
Output100200{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 :
Output010203040
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.