Les générateurs sont une fonctionnalité introduite dans ES6 qui permet de créer des fonctions spéciales avec la capacité de mettre en pause leur exécution en retournant un résultat intermédiaire.
Ils sont déclarés avec le mot clef function *, et utilisent le mot clef yield pour retourner un résultat intermédiaire.
L’itérateur posséde une fonction next, qui va exécuter le générateur jusqu’au premier yield.
Il va ensuite reprendre le genérateur là ou il s’était arrêté sur les appels suivants, et ainsi de suite.
next retourne un objet contenant deux clefs :
value, indiquant la valeur retournée par yield.
done: un booléen indiquant si le générateur est arrivé à la fin de son éxéctution.
iterator.next(); // { value: 0, done: false }
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
// ... 97 calls later
iterator.next(); // { value: 99, done: false }
iterator.next(); // { value: 100, done: true }
iterator.next(); // { value: 100, done: true }
Une fois que le générateur a été totalement exécuté, next continue de retourner le dernier résultat.
Remarque:
Un générateur se termine toujours par la valeur retournée par return, ou undefined si aucun return n’est spécifié.
function*gen() {
yield5;
}
équivaut à
function*gen() {
yield5;
returnundefined;
}
Et
function*gen() {
returnyield5;
}
ne retourne pas 5 mais la valeur passée au dernier next.
Nous avons donc une fonction capable de produire des valeurs sur demande.
Mais, les générateurs, ne s’arrêtent pas là. En plus de donner des valeurs, ils peuvent en recevoir.
function*addGenerator(i) {
while (true) {
i +=yield i;
}
}
Ok, qu’est ce que l’on a là ?
Un indice: yield fonctionne dans les deux sens et next accepte un paramètre.
constiterator=addGenerator(0);
iterator.next(); // { value: 0, done: false }
iterator.next(2); // { value: 2, done: false }
iterator.next(5); // { value: 7, done: false }
La ligne i += yield i; se lit de droite à gauche. D’abord, i est retourné.
Le générateur attend alors que next soit appelé.
Puis il reçoit la valeur passée à next et résume son exécution, en l’additionnant à i.
Nous avons donc la possibilité de communiquer avec le générateur, il nous donne des valeurs, et on lui en passe d’autres en réponses.
Remarque:
La valeur passée au premier next, est toujours ignorée, c’est la valeur passée au générateur qui est prise en compte.
Mais comment faire si le générateur nous donne une valeur que l’on juge incorrecte ?
On doit pouvoir lui dire qu’il y a une erreur.
Reprenons l’exemple précédent.
Si l’on appelle next avec une chaîne de caractères, par exemple :
iterator.next("hello"); // { value: NaN, done }
iterator.next(5); // { value: NaN, done }
iterator.next(1); // { value: NaN, done }
Le générateur se retrouve dans un état où il est devenu complètement inutile, 'hello' + 7 donnant NaN, i est devenu NaN.
Et ensuite peu importe l’opération, il reste NaN.
Comment signaler au générateur qu’il est dans un état incorrect ? En lui passant une erreur grâce à la méthode throw.
iterator.throw(newError("yielded value is not a number"));
// Error: yielded value is not a number';
Ok, iterator.throw nous a rejeté l’erreur directement, pas très utile.
Cependant, si on rappelle iterator.next (après avoir catché l’erreur bien sûr) :
On constate que l’itérateur est maintenant terminé : done: true.
Mais l’idéal serait que le générateur soit capable de gérer l’erreur.
Ca tombe bien, il peut, avec try catch.
function*addGeneratorV2() {
let i =0;
while (true) {
try {
i +=yield i;
} catch (error) {
i =0;
}
}
}
En résumé, yield transmet la valeur sur sa droite à iterator.next.
Et retourne la valeur qu’il reçoit de iterator.next.
Sauf si c’est iterator.throw qui est appelé, auquel cas il jette l’erreur reçue.
Imaginons que vous ayez à exécuter plusieurs fois le même code dans plusieurs générateurs.
Plutôt que de copier coller ce code x fois, vous en faites un autre générateur, que vous appelez ensuite dans un générateur.
function*gen1() {
yield1;
yield2;
}
function*gen() {
yieldgen1();
}
Problème : next ne retourne pas la première valeur de gen1, mais l’itérateur retourné par celui ci.
constit=gen();
constn= it.next(); // { value: {}, done: false }
n.value.next(): // { value: 1, done: false }
Logique, mais pas pratique.
Pour ce cas de figure, il existe yield *. Il prend un générateur, et qui l’exécute dans le contexte courant.
function*gen() {
yield*gen1();
constit=gen();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
}
Notez que si yield * est appelé avec autre chose qu’un itérateur, il aura le même effet que yield.
Grâce à yield*, il devrait être possible de créer un générateur récursif, c’est à dire qui se rappelle lui même.
Ainsi :
function*countGenerator(i) {
while (i >=100) {
yield i++;
}
}
Peut se réécrire en :
function*countGenerator(i) {
if (i >=100) {
return i;
}
returnyield*countGenerator(++i);
}
Sauf que :
constit=countGenerator(0);
it.next(); // { value: 100, done: true }
On constate que l’intégralité du générateur est exécutée en une fois, et on ne récupère que le dernier résultat.
Pourquoi ? Je dois bien avouer que je n’en ai pas la moindre idée.
Autre chose à noter, les générateurs ne supportent pas le tail call optimization.
En résumé cela signifie que l’on peut très vite se retrouver avec un dépassement de pile stack overflow.
Bref, ne faites pas de récursion avec les générateurs.