L’auteur a choisi le Open Internet/Free Speech Fund comme récipiendaire d’un don dans le cadre du programme Write for Donations.
Dans ECMAScript 2015, les générateurs ont été introduits au langage JavaScript. Un générateur est un processus qui peut être interrompu et repris et qui peut produire des valeurs multiples. Un générateur en JavaScript est constitué d’une fonction de génération, qui renvoie un objet générateur
itérable.
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, qui peuvent être utilisés pour mettre en œuvre un défilement infini sur le front d’une application web pour fonctionner 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 de manière plus directe et plus lisible. Bien que l’async/await
soit un moyen plus répandu pour traiter les cas d’utilisation asynchrone simples et courants, comme la récupération de données à partir d’une API, les générateurs ont des fonctionnalités plus avancées qui rendent l’apprentissage de leur utilisation intéressant.
Dans cet article, nous verrons comment créer des fonctions de générateur, comment itérer sur les objets du générateur
, la différence entre le rendement
et le retour
dans un générateur, et d’autres aspects du travail avec les générateurs.
Une fonction de générateur est une fonction qui renvoie un objet générateur
, et est définie par le mot-clé function
suivi d’un astérisque (*
), comme indiqué ci-dessous :
// Generator function declaration
function* generatorFunction() {}
Parfois, vous verrez l’astérisque à côté du nom de la fonction, par opposition au mot-clé de la fonction, comme la fonction *generatorFunction()
. Cela fonctionne de la même manière, mais function*
est une syntaxe plus largement acceptée.
Les fonctions de générateur peuvent également être définies dans une expression, comme les fonctions régulières :
// Generator function expression
const generatorFunction = function*() {}
Les générateurs peuvent même être les méthodes d’un objet ou d’une classe :
// Generator as the method of an object
const generatorObj = {
*generatorMethod() {},
}
// Generator as the method of a class
class GeneratorClass {
*generatorMethod() {}
}
Les exemples présentés tout au long de cet article utiliseront la syntaxe de déclaration de la fonction de générateur.
Remarque : contrairement aux fonctions ordinaires, les générateurs ne peuvent pas être construits avec le nouveau
mot-clé, ni être utilisés en conjonction avec les fonctions de flèches.
Maintenant que vous savez comment déclarer les fonctions du générateur, examinons les objets itératifs du générateur
qu’ils renvoient.
Traditionnellement, les fonctions en JavaScript s’exécutent jusqu’au bout, 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 la valeur 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 values
function 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 de générateur, cependant, ne renvoie pas une valeur immédiatement, mais plutôt un objet générateur
itérable. Dans l’exemple suivant, nous déclarons une fonction et lui donnons une valeur de retour unique, comme une fonction standard :
// Declare a generator function with a single return value
function* generatorFunction() {
return 'Hello, Generator!'
}
Lorsque nous invoquons la fonction générateur, elle renvoie l’objet générateur
, que nous pouvons attribuer à une variable :
// Assign the Generator object to generator
const generator = generatorFunction()
S’il s’agissait d’une fonction régulière, nous nous attendrions à ce que le générateur
nous donne la chaîne renvoyée dans la fonction. Cependant, ce que nous obtenons en réalité, c’est un objet en état de suspension
. Le générateur
d’appel donnera donc une sortie similaire à ce qui suit :
OutputgeneratorFunction {<suspended>}
__proto__: Generator
[[GeneratorLocation]]: VM272:1
[[GeneratorStatus]]: "suspended"
[[GeneratorFunction]]: ƒ* generatorFunction()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]
L’objet générateur
renvoyé par la fonction est un itérateur. Un itérateur est un objet qui dispose d’une méthode next()
qui est utilisée pour itérer à travers une séquence de valeurs. La méthode next()
retourne un objet avec des propriétés value
et done
. value
représente la valeur retournée, et done
indique si l’itérateur a parcouru toutes ses valeurs ou non.
Sachant cela, appelons next()
sur notre générateur
et obtenons la valeur et l’état actuels de l’itérateur :
// Call the next method on the Generator object
generator.next()
Cela donnera le résultat suivant :
Output{value: "Hello, Generator!", done: true}
La valeur renvoyée par l’appel de next()
est Hello, Generator !
et l’état de done
est true
, car cette valeur provient d’un retour
qui a fermé l’itérateur. Lorsque l’itérateur est terminé, l’état de la fonction du générateur passe de suspendu
à fermé
. En appelant à nouveau le générateur
, vous obtiendrez ce qui suit :
OutputgeneratorFunction {<closed>}
Pour l’instant, nous n’avons fait que démontrer comment une fonction de générateur peut être un moyen plus complexe d’obtenir la valeur de retour
d’une fonction. Mais les fonctions de générateur ont également des caractéristiques uniques qui les distinguent des fonctions normales. Dans la prochaine section, nous apprendrons à connaître l’opérateur de rendement
et verrons comment un générateur peut s’arrêter et reprendre l’exécution.
rendement
Les générateurs introduisent un nouveau mot-clé dans JavaScript : yield
. yield
peut mettre en pause une fonction du générateur et renvoyer la valeur qui suit le rendement
, offrant ainsi un moyen léger d’itération des valeurs.
Dans cet exemple, nous allons interrompre trois fois la fonction du générateur avec des valeurs différentes, et retourner une valeur à la fin. Ensuite, nous affecterons notre objet générateur
à la variable de générateur
.
// Create a generator function with multiple yields
function* generatorFunction() {
yield 'Neo'
yield 'Morpheus'
yield 'Trinity'
return 'The Oracle'
}
const generator = generatorFunction()
Maintenant, lorsque nous appelons next()
sur la fonction de générateur, elle s’arrêtera à chaque fois qu’elle rencontrera un rendement
. done
sera mis à false
après chaque rendement
, indiquant que le générateur n’a pas terminé. Lorsqu’il rencontrera un rendement
, ou qu’il n’y aura plus de rendement
rencontré dans la fonction, done
portera la valeur true
, et le générateur sera terminé.
Utilisez la méthode next()
quatre fois de suite :
// Call next four times
generator.next()
generator.next()
generator.next()
generator.next()
Cela donnera les quatre lignes de résultats 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 ne nécessite pas de retour
; s’il est omis, la dernière itération retournera {valeur : indéfini, fait : vrai}
, comme tout appel ultérieur à next()
après la fin d’un générateur.
En utilisant la méthode next()
, nous avons itéré manuellement à travers l’objet générateur
en recevant toutes les valeurs
et les propriétés done
de l’objet complet. Cependant, tout comme Array
, Map
, and Set
, un générateur
suit le protocole d’itération, et peut être itéré avec pour...de
:
// Iterate over Generator object
for (const value of generator) {
console.log(value)
}
Il en résultera ce qui suit :
OutputNeo
Morpheus
Trinity
L’opérateur d’étalement peut également être utilisé pour attribuer les valeurs d’un générateur
à un tableau.
// Create an array from the values of a Generator object
const values = [...generator]
console.log(values)
Cela donnera le tableau suivant :
Output(3) ["Neo", "Morpheus", "Trinity"]
Le spread et le for...of
ne tiennent pas compte du rendement
dans les valeurs (dans ce cas, il s’agirait de « l'Oracle »
).
Remarque : 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 la diffusion ou for...of
directement sans créer une boucle infinie.
Comme nous l’avons vu, un générateur peut avoir sa propriété done
réglée sur true
et son statut réglé sur closed
en répétant toutes ses valeurs. Il existe deux autres moyens d’annuler immédiatement un générateur : avec le return()
, et avec la méthode throw()
.
Avec return()
, le générateur peut être arrêté à tout moment, tout comme si une déclaration de retour
avait été dans le corps de la fonction. Vous pouvez faire passer un argument dansreturn()
, ou laissez le champ vide pour une valeur non définie.
Pour démontrer le retour()
, nous allons créer un générateur avec quelques valeurs de rendement
mais sans retour
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 générateur
juste après cela, nous allons maintenant obtenir la valeur passée et done
fixée sur true
. Tout appel supplémentaire à next()
donnera la réponse du générateur complétée par défaut avec une valeur non définie.
Pour le démontrer, appliquez les trois méthodes suivantes surgenerator
:
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 générateur
à compléter et à ignorer tout autre mot-clé de rendement
. Ceci est particulièrement utile dans la programmation asynchrone lorsque vous devez rendre des 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 Promesse.
Si le corps d’une fonction de générateur a un moyen de détecter et de traiter les erreurs, vous pouvez utiliser la méthode throw()
pour lancer une erreur dans le générateur. Cela permet de démarrer le générateur, d’y introduire l’erreur et d’y mettre fin.
Pour le démontrer, nous allons faire un essai... attraper
à l’intérieur du corps de fonction du générateur et enregistrer une erreur si elle est trouvée :
// Define a generator function with a try...catch
function* generatorFunction() {
try {
yield 'Neo'
yield 'Morpheus'
} catch (error) {
console.log(error)
}
}
// Invoke the generator and throw an error
const generator = generatorFunction()
Maintenant, nous allons exécuter la méthode next()
, suivie de throw()
:
generator.next()
generator.throw(new Error('Agent Smith!'))
Cela donnera le résultat suivant :
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é rattrapée par l'essai... attrapée
et enregistrée dans la console.
Le tableau suivant présente une liste des méthodes qui peuvent être utilisées sur les objets générateurs
:
Méthode | Description |
---|---|
next() |
Retourne la valeur suivante 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 énumère les états possibles d’un objet générateur
:
Statut | Description |
---|---|
suspendu |
Le générateur a arrêté l’exécution mais n’a pas terminé |
fermé |
Le générateur s’est terminé soit par une erreur, soit par un retour, soit par une itération à travers toutes les valeurs |
rendement
En plus de l’opérateur de rendement
régulier, 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é dans un générateur, il se rend à l’intérieur du générateur délégué et commence à itérer à travers tous les rendements
jusqu’à ce que ce générateur soit fermé. Cela peut être utilisé pour séparer différentes fonctions de génération afin d’organiser sémantiquement votre code, tout en ayant tous leurs rendements
itérables dans le bon ordre.
Pour le démontrer, nous pouvons créer deux fonctions de générateur, dont l’une yield*
sur l’autre :
// Generator function that will be delegated to
function* delegate() {
yield 3
yield 4
}
// Outer generator function
function* begin() {
yield 1
yield 2
yield* delegate()
}
Ensuite, nous allons itérer à travers la fonction de génération begin()
:
// Iterate through the outer generator
const 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 :
Output1
2
3
4
Le générateur extérieur a donné les valeurs 1
et 2
, puis a été délégué à l’autre générateur avec yield*
, qui a donné 3
et 4
.
yield*
peut également déléguer à tout objet itérable, tel qu’un objet Array ou Map. La délégation de rendement peut être utile pour organiser le code, puisque toute fonction au sein d’un générateur qui souhaite utiliser yield
doit également être un générateur.
L’un des aspects utiles des générateurs est la capacité de travailler avec des flux et des collections de données 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 d’une unité.
Dans le bloc de code suivant, nous définissons cette fonction de générateur et nous lançons ensuite le générateur :
// Define a generator function that increments by one
function* incrementer() {
let i = 0
while (true) {
yield i++
}
}
// Initiate the generator
const counter = incrementer()
Maintenant, itérez les valeurs en utilisant next()
:
// Iterate through the values
counter.next()
counter.next()
counter.next()
counter.next()
Cela donnera le résultat suivant :
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 alors que la propriété done
reste false
, ce qui garantit 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, il faut être prudent quant à la manière d’appeler le générateur. Si vous utilisez spread ou for...of
sur un flux de données infini, vous continuerez à itérer sur une boucle infinie d’un seul coup, ce qui provoquera un crash de l’environnement.
Pour un exemple plus complexe d’un flux de données infini, nous pouvons créer une fonction de générateur de Fibonacci. La séquence de Fibonacci, qui additionne continuellement les deux valeurs précédentes, peut être écrite en utilisant une boucle infinie dans un générateur comme suit :
// Create a fibonacci generator function
function* fibonacci() {
let prev = 0
let next = 1
yield prev
yield next
// Add previous and next values and yield them forever
while (true) {
const newVal = next + prev
yield newVal
prev = next
next = newVal
}
}
Pour tester cela, nous pouvons passer en boucle un nombre fini et imprimer la séquence de Fibonacci sur la console.
// Print the first 10 values of fibonacci
const fib = fibonacci()
for (let i = 0; i < 10; i++) {
console.log(fib.next().value)
}
Cela donnera le résultat :
Output0
1
1
2
3
5
8
13
21
34
La capacité à travailler avec des ensembles de données infinis est l’une des raisons pour lesquelles les générateurs sont si puissants. Cela peut être utile pour des exemples comme la mise en œuvre du défilement infini sur le front d’une application web.
Tout au long de cet article, nous avons utilisé des générateurs comme itérateurs, et nous avons obtenu des valeurs à chaque itération. En plus de produire des valeurs, les producteurs peuvent également consommer des valeurs provenant de next()
Dans ce cas, yield
contiendra une valeur.
Il est important de noter que le premier next()
qui est appelé ne passera pas une valeur, mais ne fera que démarrer le générateur. Pour le démontrer, nous pouvons enregistrer la valeur de yield
et appeler next()
plusieurs fois avec certaines valeurs.
function* generatorFunction() {
console.log(yield)
console.log(yield)
return 'The end'
}
const generator = generatorFunction()
generator.next()
generator.next(100)
generator.next(200)
Cela donnera le résultat suivant :
Output100
200
{value: "The end", done: true}
Il est également possible d’ensemencer le générateur avec une valeur initiale. Dans l’exemple suivant, nous allons faire une boucle for
et passer chaque valeur dans la méthode next()
, mais passer également un argument à la fonction initiale :
function* generatorFunction(value) {
while (true) {
value = yield value * 10
}
}
// Initiate a generator and seed it with an initial value
const generator = generatorFunction(0)
for (let i = 0; i < 5; i++) {
console.log(generator.next(i).value)
}
Nous allons récupérer la valeur de next()
et donner une nouvelle valeur à la prochaine itération, qui est la valeur précédente multipliée par dix. Cela donnera le résultat :
Output0
10
20
30
40
Une autre façon de gérer le démarrage d’un générateur consiste à envelopper le générateur dans une fonction qui appellera toujours next()
une fois avant de faire autre chose.
async
/await
avec les générateursUne fonction asynchrone est un type de fonction disponible en JavaScript ES6+ qui rend le travail avec des données asynchrones plus facile à comprendre en les faisant apparaître comme 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 d’une programmation asynchrone de cette manière peut accroître la flexibilité de votre code.
Dans cette section, nous allons montrer un exemple de reproduction d’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.
Commencez par définir une fonction asynchrone appelée getUsers
qui va chercher des données dans l’API et renvoie un tableau d’objets, puis appelez getUsers
:
const getUsers = async function() {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const json = await response.json()
return json
}
// Call the getUsers function and log the response
getUsers().then(response => console.log(response))
Cela donnera des données JSON similaires à celles qui suivent :
Output[ {id: 1, name: "Leanne Graham" ...},
{id: 2, name: "Ervin Howell" ...},
{id: 3, name": "Clementine Bauch" ...},
{id: 4, name: "Patricia Lebsack"...},
{id: 5, name: "Chelsey Dietrich"...},
...]
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 de 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
.
const getUsers = asyncAlt(function*() {
const response = yield fetch('https://jsonplaceholder.typicode.com/users')
const json = yield response.json()
return json
})
// Invoking the function
getUsers().then(response => console.log(response))
Comme on peut le voir, il semble presque identique à l’implémentation async
/await
, sauf qu’il y a une fonction génératrice qui est passée dans le système et qui donne des valeurs.
Nous pouvons maintenant créer une fonction asyncAlt
qui ressemble à une fonction asynchrone. asyncAlt
a une fonction génératrice comme paramètre, qui est notre fonction qui produit les promesses que fetch
renvoie. asyncAlt
retourne une fonction elle-même, et résout chaque promesse qu’elle trouve jusqu’à la dernière :
// Define a function named asyncAlt that takes a generator function as an argument
function asyncAlt(generatorFunction) {
// Return a function
return function() {
// Create and assign the generator object
const generator = generatorFunction()
// Define a function that accepts the next iteration of the generator
function resolve(next) {
// If the generator is closed and there are no more values to yield,
// resolve the last value
if (next.done) {
return Promise.resolve(next.value)
}
// If there are still values to yield, they are promises and
// must be resolved.
return Promise.resolve(next.value).then(response => {
return resolve(generator.next(response))
})
}
// Begin resolving promises
return resolve(generator.next())
}
}
Cela donnera le même résultat que la version async
/await
:
Output[ {id: 1, name: "Leanne Graham" ...},
{id: 2, name: "Ervin Howell" ...},
{id: 3, name": "Clementine Bauch" ...},
{id: 4, name: "Patricia Lebsack"...},
{id: 5, name: "Chelsey Dietrich"...},
...]
Notez que cette mise en œuvre sert à démontrer comment les générateurs peuvent être utilisés à la place de d’async
/await
, et n’est pas une conception prête pour la production. Elle n’est pas configurée pour le traitement des erreurs et n’a pas la capacité de passer des paramètres dans les valeurs obtenues. Bien que cette méthode puisse ajouter de la flexibilité à votre code, async/await
sera souvent un meilleur choix, car elle permet d’abstraire les détails de l’implémentation et de se concentrer sur l’écriture de code productif.
Les générateurs sont des processus qui peuvent s’arrêter et reprendre leur exécution. Ils constituent une fonction puissante et polyvalente de JavaScript, bien qu’ils ne soient pas couramment utilisés. Dans ce tutoriel, nous nous sommes familiarisés avec les fonctions et les objets des générateurs, 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é un moyen de mettre en œuvre un code asynchrone sans rappels imbriqués ni longues chaînes de promesses.
Si vous souhaitez en savoir plus sur la syntaxe JavaScript, consultez nos tutoriels Comprendre ça, lier, appeler et appliquer en JavaScript et Comprendre les objets Map et Set en JavaScript.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!