Générateur ES6, un style d'expression de contrôle de flux asynchrone apparemment synchrone

Cet article est partagé par la communauté Huawei Cloud « Semaine de lecture de mars·JavaScript que vous ne connaissez pas | ES6 Generator, un style d'expression de contrôle de processus asynchrone apparemment synchrone », auteur : Ye Yiyi.

Constructeur

pause complète

Il existe une hypothèse sur laquelle les développeurs JavaScript s'appuient presque universellement dans leur code : une fois qu'une fonction commence à s'exécuter, elle s'exécutera jusqu'à la fin, et aucun autre code ne peut l'interrompre et l'insérer entre les deux.

ES6 introduit un nouveau type de fonction qui n'est pas conforme à cette fonctionnalité de bout en bout. Ce nouveau type de fonction est appelé générateur.

variable x = 1 ;

fonction foo() {
  x++ ;
  bar(); // <-- cette ligne s'étend entre les instructions x++ et console.log(x)
  console.log('x:', x);
}

barre de fonctions() {
  x++ ;
}

foo(); //x : 3

Que se passe-t-il si bar() n'est pas là ? Évidemment, le résultat sera 2 et non 3. Le résultat final est 3, donc bar() s'exécutera entre x++ et console.log(x).

Mais JavaScript n’est pas préemptif et n’est pas (encore) multithread. Cependant, il serait toujours possible d'implémenter de telles interruptions de manière coopérative (concurrence) si foo() lui-même pouvait d'une manière ou d'une autre indiquer une pause à ce stade du code.

Voici le code ES6 pour implémenter la concurrence coopérative :

variable x = 1 ;

fonction* foo() {
  x++ ;
  rendement ; // Pause !
  console.log('x:', x);
}

barre de fonctions() {
  x++ ;
}
//Construisez un itérateur pour contrôler ce générateur
var it = foo();

//Démarrez foo() ici !
it.next();
console.log('x:', x); // 2
bar();
console.log('x:', x); // 3
it.next(); //x : 3
  • L'opération it = foo() n'exécute pas le générateur *foo(), mais construit uniquement un itérateur (iterator), qui contrôle son exécution.
  • *foo() fait une pause à l'instruction rendement, moment auquel le premier appel it.next() se termine. À ce stade, *foo() est toujours en cours d'exécution et actif, mais dans un état suspendu.
  • Le dernier appel it.next() reprend l'exécution du générateur *foo() à partir de l'endroit où il a été interrompu et exécute l'instruction console.log(..), qui utilise la valeur actuelle de x, 3.

Un générateur est un type particulier de fonction qui peut être démarré et arrêté une ou plusieurs fois, mais ne doit pas nécessairement être complété.

entrée et sortie

Une fonction génératrice est une fonction spéciale qui possède encore certaines propriétés de base des fonctions. Par exemple, il peut toujours accepter des paramètres (c'est-à-dire une entrée) et renvoyer des valeurs (c'est-à-dire une sortie).

fonction* foo(x, y) {
  renvoyer x * y ;
}

var it = foo(6, 7);
var res = it.next();

res.valeur ; // 42

Passez les paramètres réels 6 et 7 à *foo(..) en tant que paramètres x et y respectivement. *foo(..) renvoie 42 au code appelant.

plusieurs itérateurs

Chaque fois que vous créez un itérateur, vous créez implicitement une instance du générateur. C'est l'instance du générateur qui est contrôlée via cet itérateur.

Plusieurs instances du même générateur peuvent fonctionner simultanément et peuvent même interagir les unes avec les autres :

fonction* foo() {
  var x = rendement 2 ;
  z++;
  var y = rendement x * z ;
  console.log(x, y, z);
}

var z = 1 ;

var it1 = foo();
var it2 = foo();

var val1 = it1.next().value; // 2 <-- rendement 2
var val2 = it2.next().value; // 2 <-- rendement 2

val1 = it1.next(val2 * 10).value; // 40 <-- x:20, z:2
val2 = it2.next(val1 * 5).value; // 600 <-- x:200, z:3

it1.next(val2 / 2); // y : 300
// 20300 3
it2.next(val1 / 4); // y:10
// 200 10 3

Résumez brièvement le processus d’exécution :

(1) Deux instances de *foo() sont démarrées en même temps, et les deux next() obtiennent respectivement la valeur 2 de l'instruction rendement 2.

(2) val2 * 10, qui vaut 2 * 10, est envoyé à la première instance du générateur it1, donc x obtient la valeur 20. z augmente de 1 à 2, puis 20 * 2 est émis via le rendement, définissant val1 sur 40.

(3) val1 * 5, qui vaut 40 * 5, est envoyé à la deuxième instance du générateur it2, donc x obtient la valeur 200. z est à nouveau incrémenté de 2 à 3, puis 200 * 3 est émis via le rendement, définissant val2 sur 600.

(4) val2 / 2, qui vaut 600 / 2, est envoyé à la première instance du générateur it1, donc y obtient la valeur 300, puis les valeurs de xyz sont imprimées respectivement sous la forme 20300 3.

(5) val1 / 4, qui est 40 / 4, est envoyé à la deuxième instance du générateur it2, donc y obtient la valeur 10, puis les valeurs de xyz sont imprimées sous la forme 200 10 3 respectivement.

Le générateur produit de la valeur

Producteurs et itérateurs

Supposons que vous souhaitiez générer une séquence de valeurs, chacune ayant une relation spécifique avec la précédente. Pour y parvenir, il faut un producteur avec état capable de se souvenir de la dernière valeur qu'il a produite.

Un itérateur est une interface bien définie permettant d'obtenir étape par étape une séquence de valeurs d'un producteur. L'interface de l'itérateur JavaScript consiste à appeler next() chaque fois que vous souhaitez obtenir la valeur suivante du producteur.

L'interface d'itérateur standard peut être implémentée pour les générateurs de séquences numériques :

var quelque chose = (fonction () {
  var suivantValide ;

  retour {
    // boucle for..of requise
    [Symbol.iterator] : fonction () {
      rends ceci ;
    },

    // Méthode d'interface d'itérateur standard
    suivant : fonction () {
      if (nextVal === non défini) {
        valeur suivante = 1 ;
      } autre {
        valeur suivante = 3 * valeur suivante + 6 ;
      }

      return { done : false, valeur : nextVal } ;
    },
  } ;
})();

quelque chose.next().value; // 1
quelque chose.next().value; // 9
quelque chose.next().value; // 33
quelque chose.next().value; // 105

L'appel next() renvoie un objet. Cet objet a deux propriétés : done est une valeur booléenne qui identifie l'état d'achèvement de l'itérateur ; value place la valeur de l'itération.

itérable

iterable est un objet qui contient un itérateur qui peut être itéré sur ses valeurs.

À partir d'ES6, la façon d'extraire un itérateur d'un itérable est que l'itérable doit prendre en charge une fonction dont le nom est la valeur de symbole ES6 spécialisée Symbol.iterator. Lorsque cette fonction est appelée, elle renvoie un itérateur. Habituellement, chaque appel renvoie un nouvel itérateur, bien que cela ne soit pas obligatoire.

var une = [1, 3, 5, 7, 9];

pour (var v de a) {
  console.log(v);
}
// 1 3 5 7 9

a dans l'extrait de code ci-dessus est un itérable. La boucle for..of appelle automatiquement sa fonction Symbol.iterator pour construire un itérateur.

pour (var v de quelque chose) {
  ..
}

La boucle for..of s'attend à ce que quelque chose soit un itérable, elle recherche donc et appelle sa fonction Symbol.iterator.

itérateur de générateur

Le générateur peut être considéré comme un producteur de valeurs. Nous extrayons une valeur à la fois via l'appel next() de l'interface itérateur.

Les générateurs eux-mêmes ne sont pas itérables. Lorsque vous exécutez un générateur, vous obtenez un itérateur :

fonction *foo(){ .. }

var it = foo();

Le précédent producteur de séquences de nombres infinis peut être implémenté via un générateur, similaire à celui-ci :

fonction* quelque chose() {
  var suivantValide ;

  tandis que (vrai) {
    if (nextVal === non défini) {
      valeur suivante = 1 ;
    } autre {
      valeur suivante = 3 * valeur suivante + 6 ;
    }

    rendement nextVal ;
  }
}

Étant donné que le générateur fait une pause à chaque rendement, l'état (portée) de la fonction *something() est conservé, ce qui signifie qu'aucune fermeture n'est nécessaire pour maintenir un état variable entre les appels.

Générateur d'itérateurs asynchrones

fonction foo(x, y) {
  ajax('http://some.url.1/? x=' + x + '&y=' + y, fonction (erreur, données) {
    si (erreur) {
      // Lance une erreur à *main()
      it.throw(err);
    } autre {
      //Restaurer *main() avec les données reçues
      it.next(données);
    }
  });
}

fonction* main() {
  essayer {
    var texte = rendement foo(11, 31);
    console.log(texte);
  } attraper (erreur) {
    console.erreur(erreur);
  }
}

var it = main();

//Commencer ici!
it.next();

Dans rendement foo(11,31), foo(11,31) est d'abord appelé, qui ne renvoie aucune valeur (c'est-à-dire renvoie undéfini), donc un appel est effectué pour demander les données, mais ce qui est réellement fait ensuite est rendement indéfini.

Le rendement n'est pas utilisé ici dans le sens de transmission de messages, mais uniquement pour le contrôle de flux afin d'implémenter la pause/le blocage. En fait, il y aura toujours un message qui passera, mais il ne s'agira que d'un message unidirectionnel après la reprise du fonctionnement du générateur.

Jetez un oeil à foo(..). Si cette requête Ajax réussit, nous appelons :

it.next(données);

Cela reprend le générateur avec les données de réponse, ce qui signifie que l'expression de rendement en pause a reçu cette valeur directement. Cette valeur est ensuite attribuée au texte de la variable locale à mesure que le code du générateur continue de s'exécuter.

Résumer

Résumons le contenu principal de cet article :

  • Generator est un nouveau type de fonction dans ES6. Il ne s'exécute pas toujours jusqu'au bout comme les fonctions ordinaires. Au lieu de cela, un générateur peut être mis en pause pendant son fonctionnement (en préservant entièrement son état) et repris ultérieurement là où il avait été mis en pause.
  • La paire rendement/next(..) n'est pas seulement un mécanisme de contrôle, mais aussi un mécanisme de transmission de messages bidirectionnel. rendement .. L'expression fait essentiellement une pause et attend une valeur, et l'appel next(..) suivant renvoie une valeur (ou implicitement non définie) à l'expression de rendement en pause.
  • Le principal avantage des générateurs en matière de flux de contrôle asynchrone est que le code à l'intérieur du générateur est une séquence d'étapes exprimant une tâche de manière naturellement synchrone/séquentielle. L'astuce consiste à masquer l'asynchronie possible derrière le mot-clé rendement et à déplacer l'asynchronie vers la partie du code qui contrôle l'itérateur du générateur.
  • Les générateurs maintiennent un modèle de code séquentiel, synchrone et bloquant pour le code asynchrone, ce qui permet au cerveau de suivre le code plus naturellement, résolvant ainsi l'un des deux principaux défauts de l'asynchronie basée sur le rappel.

 

Cliquez pour suivre et découvrir les nouvelles technologies de Huawei Cloud dès que possible~

 

La première mise à jour majeure de JetBrains 2024 (2024.1) est open source. Même Microsoft prévoit de la payer. Pourquoi est-elle encore critiquée pour son open source ? [Récupéré] Le backend de Tencent Cloud s'est écrasé : un grand nombre d'erreurs de service et aucune donnée après la connexion à la console. L'Allemagne doit également être "contrôlable de manière indépendante". Le gouvernement de l'État a migré 30 000 PC de Windows vers Linux deepin-IDE et a finalement réussi démarrage ! Visual Studio Code 1.88 est sorti. Bon gars, Tencent a vraiment transformé Switch en une "machine d'apprentissage pensante". Le bureau à distance RustDesk démarre et reconstruit le client Web. La base de données de terminaux open source de WeChat basée sur SQLite, WCDB, a reçu une mise à niveau majeure.
{{o.name}}
{{m.nom}}

Je suppose que tu aimes

Origine my.oschina.net/u/4526289/blog/11051652
conseillé
Classement