Dans ce tutoriel je vous propose de découvrir les générateurs en JavaScript et les fonctions function*
qui ont la particularité de pouvoir être quittées et reprise à tout moment.
function* animals {
yield "chien"
yield "chat"
yield "rat"
}
Lorsque l'on exécute cette fonction on obtiendra un object de type Generator
qui sera itérable.
const gen = animals(); // Generator
gen.next(); // {value: 'chien', done: false}
gen.next(); // {value: 'chat', done: false}
gen.next(); // {value: 'rat', done: false}
gen.next(); // {value: undefined, done: true}
Vu que l'objet est itérable on peut l'utiliser dans une boucle ou dans une fonction qui accepte ce type d'argument.
// Dans une boucle
for (const animal of animals()) {
console.log(animal); // "chien", "chat", "rat"
}
// Dans une fonction
Array.from(animals()); // ["chien", "chat", "rat"]
Enfin, il est aussi possible de combiner des générateurs ensembles à l'aide du mot clefs yield*
function* animals() {
yield "chien";
yield* insectes();
yield "chat";
yield "rat";
}
function* insectes() {
yield "scarabé";
yield "fourmi";
yield "papillon";
}
Array.from(animals()); // ['chien', 'scarabé', 'fourmi', 'papillon', 'chat', 'rat']
Quel objectif ?
On peut maintenant se demander quel est l'intérêt des générateurs en JavaScript par rapport à un simple tableau. Le principal avantage va principalement concerner la performance lorsqu'il s'agit d'itérer sur un grand nombre de résultat.
Par exemple, si on souhaite lire une longue chaine ligne par ligne, les générateurs permettent d'éviter de générer un long tableau et de ne lire les éléments que lorsqu'ils sont demandées.
function* readLine(s) {
let currentPosition = 0;
while (true) {
const nextNewLine = s.indexOf("\n", currentPosition);
if (nextNewLine === -1) {
yield s.slice(currentPosition);
break;
}
yield s.slice(currentPosition, nextNewLine);
currentPosition = nextNewLine + 1;
}
}
Il est aussi possible de les utiliser pour boucler sur des éléments dans un système paginé.
function* listUser() {
let page = 0;
for (const page of queryUsers(page)) {
for (const user of page) {
yield user;
}
page++;
}
}
Les générateurs permettent aussi de créer des tableaux de taille infinie qui vont se générer au fur à mesure que l'on demande ses éléments.
function* idMaker() {
let i = 0;
while (true) {
yield i++;
}
}
Générateur asynchrones
Les générateurs peuvent aussi être asynchrone et leur résultat pourra être utilisé dans un for await
.
async function* fetchUsers() {
let page = 0;
while (page <= 1) {
const items = await fetch(
"https://jsonplaceholder.typicode.com/users?_limit=10&_start=" + 10 * page
).then((r) => r.json());
// Plus de résultats, on sort de la boucle
if (items.length === 0) {
return;
}
yield items;
page++;
}
}
Dans ce cas là, le résultat de l'appel à next()
sera une promesse qui pourra être utilisé dans une boucle asynchrone.
const pages = fetchUsers(); // AsyncGenerator
pages.next(); // Promise<{value: [...], done: false}>
for await (const page of pages) {
// ...
}
Cette approche est très pratique si on combine les générateurs pour par exemple créer un itérateur qui bouclera sur chaque utilisateur et qui récupèrera la bonne page au fur et à mesure.
async function* fetchUser() {
// On utilise notre premier itérateur pour boucler sur les pages, la page n'est récupérée qu'au besoin
for await (const page of fetchUsers()) {
// On yield chaque utilisateur
for (const user of page) {
yield user;
}
}
}
Le futur
Les générateurs vont continuer de s'améliorer dans le futur avec l'ajout de nouvelles méthodes qui ne sont, pour le moment pas supportées, par tous les navigateurs (Safari pour être précis). Par exemple les méthodes take() et drop() permettront de générer un nouvel itérateur qui prendra ou sautera certains éléments.
const gen = animals();
gen.drop(3).take(10).drop(5).toArray();