Tutorial

Comprendre les générateurs en JavaScript

Published on April 15, 2020
Français
Comprendre les générateurs en JavaScript

L’auteur a choisi le Open Internet/Free Speech Fund comme récipiendaire d’un don dans le cadre du programme Write for Donations.

Introduction

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.

Fonctions des 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.

Les objets générateurs

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 :

Output
generatorFunction {<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 :

Output
generatorFunction {<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.

Opérateurs de 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.

Itération sur 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 :

Output
Neo 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.

Fermer un générateur

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.

Méthodes et états des objets générateurs

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

Délégation de 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 :

Output
1 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.

Flux de données infinis

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 :

Output
0 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.

Transmettre des valeurs dans les générateurs

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 :

Output
100 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 :

Output
0 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érateurs

Une 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.

Conclusion

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.

Learn more about us


About the authors

Default avatar

Senior Technical Editor

Editor at DigitalOcean, fiction writer and podcaster elsewhere, always searching for the next good nautical pun!


Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel