One place for hosting & domains

      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éthodeDescription
      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 :

      StatutDescription
      suspenduLe 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.



      Source link


      Leave a Comment