De plus, le lecteur se voit proposer un article qui, dans le cas d'une réponse positive, peut évoluer en cycle. Si j'écris avec succès ce cycle et que le lecteur l'a maîtrisé avec succès, le code suivant sera clair non seulement sur ce qu'il fait, mais aussi sur son fonctionnement sous le capot:
while (true) {
const data = yield getNextChunk(); //
const processed = processData(data);
try {
yield sendProcessedData(processed);
showOkResult();
} catch (err) {
showError();
}
}
C'est la première partie pilote: les itérateurs et les générateurs.
Itérateurs
Ainsi, un itérateur est une interface qui fournit un accès séquentiel aux données.
Comme vous pouvez le voir, la définition ne dit rien sur les données ou les structures de mémoire. En effet, une séquence de s indéfinis peut être représentée comme un itérateur sans occuper aucun espace mémoire.
Je suggère au lecteur de répondre à la question: un tableau est-il un itérateur?
Répondre
. shift pop .
Pourquoi, alors, des itérateurs sont-ils nécessaires si un tableau, l'une des structures de base du langage, vous permet de travailler avec des données à la fois séquentiellement et dans un ordre arbitraire?
Imaginons que nous ayons besoin d'un itérateur qui implémente une séquence de nombres naturels. Ou les nombres de Fibonacci. Ou toute autre séquence sans fin . Il est difficile de placer une séquence sans fin dans un tableau; vous avez besoin d'un mécanisme pour remplir progressivement le tableau avec des données, ainsi que pour supprimer les anciennes données afin de ne pas remplir toute la mémoire du processus. Il s'agit d'une complication inutile, qui entraîne une complexité supplémentaire de mise en œuvre et de support, malgré le fait qu'une solution sans tableau peut tenir sur plusieurs lignes:
const getNaturalRow = () => {
let current = 0;
return () => ++current;
};
De plus, un itérateur peut représenter la réception de données depuis un canal externe, par exemple une websocket.
En javascript, un itérateur est tout objet qui a une méthode next () qui retourne une structure avec la valeur des champs - la valeur actuelle de l'itérateur et done - un drapeau indiquant la fin de la séquence (cette convention est décrite dans le standard de langage ECMAScript ). Un tel objet implémente l'interface Iterator. Réécrivons l'exemple précédent dans ce format:
const getNaturalRow = () => ({
_current: 0,
next() { return {
value: ++this._current,
done: false,
}},
});
Javascript a également une interface Iterable, qui est un objet qui a une méthode @@ iterator (cette constante est disponible en tant que Symbol.iterator) qui retourne un itérateur. Pour les objets implémentant une telle interface, le parcours opérateur est disponible
for..of. Réécrivons notre exemple une fois de plus, mais cette fois comme une implémentation Iterable:
const naturalRowIterator = {
[Symbol.iterator]: () => ({
_current: 0,
next() { return {
value: ++this._current,
done: this._current > 3,
}},
}),
}
for (num of naturalRowIterator) {
console.log(num);
}
// : 1, 2, 3
Comme vous pouvez le voir, nous avons dû rendre le drapeau done à un moment donné positif, sinon la boucle serait infinie.
Générateurs
Les générateurs sont devenus la prochaine étape de l'évolution des itérateurs. Ils fournissent du sucre syntaxique pour renvoyer des valeurs d'itérateur comme une valeur de fonction. Un générateur est une fonction (déclarée avec un astérisque: fonction * ) qui renvoie un itérateur. Dans ce cas, l'itérateur n'est pas renvoyé explicitement; les fonctions ne renvoient que les valeurs de l'itérateur à l'aide de l' instruction yield . Lorsque la fonction termine son exécution, l'itérateur est considéré comme terminé (les résultats des appels suivants à la méthode suivante auront le drapeau done égal à true)
function* naturalRowGenerator() {
let current = 1;
while (current <= 3) {
yield current;
current++;
}
}
for (num of naturalRowGenerator()) {
console.log(num);
}
// : 1, 2, 3
Déjà dans cet exemple simple, la principale nuance des générateurs est visible à l'œil nu: le code à l'intérieur de la fonction générateur n'est pas exécuté de manière synchrone . Le code générateur est exécuté par étapes, à la suite d'appels à next () sur l'itérateur correspondant. Voyons comment le code du générateur est exécuté dans l'exemple précédent. Nous utiliserons un curseur spécial pour marquer l'endroit où le générateur s'est arrêté.
Lorsque naturalRowGenerator est appelé, un itérateur est créé.
function* naturalRowGenerator() {
▷let current = 1;
while (current <= 3) {
yield current;
current++;
}
}
De plus, lorsque nous appelons la méthode suivante pour les trois premières fois ou, dans notre cas, nous parcourons la boucle, le curseur est positionné après l'instruction yield.
function* naturalRowGenerator() {
let current = 1;
while (current <= 3) {
yield current; ▷
current++;
}
}
Et pour tous les appels ultérieurs à next et après avoir quitté la boucle, le générateur termine son exécution et les résultats de l'appel suivant seront
{ value: undefined, done: true }
Passer des paramètres à un itérateur
Imaginez que nous devions ajouter la possibilité de réinitialiser le compteur actuel et de commencer à compter depuis le début à notre itérateur de nombres naturels.
naturalRowIterator.next() // 1
naturalRowIterator.next() // 2
naturalRowIterator.next(true) // 1
naturalRowIterator.next() // 2
Il est clair comment gérer un tel paramètre dans un itérateur auto-écrit, mais qu'en est-il des générateurs?
Il s'avère que les générateurs prennent en charge le passage de paramètres!
function* naturalRowGenerator() {
let current = 1;
while (true) {
const reset = yield current;
if (reset) {
current = 1;
} else {
current++;
}
}
}
Le paramètre passé est rendu disponible à la suite de l'instruction yield. Essayons d'ajouter de la clarté avec une approche de curseur. Lorsque l'itérateur a été créé, rien n'a changé. Ceci est suivi du premier appel à la méthode next ():
function* naturalRowGenerator() {
let current = 1;
while (true) {
const reset = ▷yield current;
if (reset) {
current = 1;
} else {
current++;
}
}
}
Le curseur s'est figé au moment où il est revenu de l'instruction yield. Lors du prochain appel à next, la valeur transmise à la fonction définira la valeur de la variable de réinitialisation. Où finit la valeur transmise au tout premier appel au suivant, puisqu'il n'y a pas encore eu d'appel à céder? Nulle part! Il se dissoudra dans l'immensité du ramasse-miettes. Si vous devez transmettre une valeur initiale au générateur, cela peut être fait en utilisant les arguments du générateur lui-même. Exemple:
function* naturalRowGenerator(start = 1) {
let current = start;
while (true) {
const reset = yield current;
if (reset) {
current = start;
} else {
current++;
}
}
}
const iterator = naturalRowGenerator(10);
iterator.next() // 10
iterator.next() // 11
iterator.next(true) // 10
Conclusion
Nous avons discuté du concept d'itérateurs et de son implémentation dans le langage javascript. Nous avons également étudié les générateurs - une construction syntaxique permettant d'implémenter facilement des itérateurs.
Bien que j'aie donné des exemples avec des séquences de nombres dans cet article, les itérateurs javascript peuvent faire beaucoup plus. Ils peuvent représenter n'importe quelle séquence de données et même de nombreuses machines à états finis. Dans le prochain article, j'aimerais parler de la façon dont vous pouvez utiliser des générateurs pour construire des processus asynchrones (coroutines, goroutines, csp, etc.).