Français

Explorez les Helpers d'Itérateurs Asynchrones JavaScript pour révolutionner le traitement de flux. Apprenez à gérer efficacement les flux de données asynchrones avec map, filter, take, drop, et plus encore.

Helpers d'Itérateurs Asynchrones JavaScript : Traitement de Flux Puissant pour les Applications Modernes

Dans le développement JavaScript moderne, la gestion des flux de données asynchrones est une exigence courante. Que vous récupériez des données depuis une API, traitiez de gros fichiers ou gériez des événements en temps réel, la gestion efficace des données asynchrones est cruciale. Les Helpers d'Itérateurs Asynchrones de JavaScript offrent un moyen puissant et élégant de traiter ces flux, proposant une approche fonctionnelle et composable pour la manipulation des données.

Que sont les Itérateurs Asynchrones et les Itérables Asynchrones ?

Avant de plonger dans les Helpers d'Itérateurs Asynchrones, comprenons les concepts sous-jacents : les Itérateurs Asynchrones (Async Iterators) et les Itérables Asynchrones (Async Iterables).

Un Itérable Asynchrone est un objet qui définit une manière d'itérer de façon asynchrone sur ses valeurs. Il le fait en implémentant la méthode @@asyncIterator, qui retourne un Itérateur Asynchrone.

Un Itérateur Asynchrone est un objet qui fournit une méthode next(). Cette méthode retourne une promesse qui se résout en un objet avec deux propriétés :

Voici un exemple simple :


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 500)); // Simule une opération asynchrone
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // Sortie : 1, 2, 3, 4, 5 (avec un délai de 500ms entre chaque)
  }
})();

Dans cet exemple, generateSequence est une fonction de générateur asynchrone qui produit une séquence de nombres de manière asynchrone. La boucle for await...of est utilisée pour consommer les valeurs de l'itérable asynchrone.

Présentation des Helpers d'Itérateurs Asynchrones

Les Helpers d'Itérateurs Asynchrones étendent la fonctionnalité des Itérateurs Asynchrones, en fournissant un ensemble de méthodes pour transformer, filtrer et manipuler les flux de données asynchrones. Ils permettent un style de programmation fonctionnel et composable, facilitant la construction de pipelines de traitement de données complexes.

Les principaux Helpers d'Itérateurs Asynchrones incluent :

Explorons chaque helper avec des exemples.

map()

Le helper map() transforme chaque élément de l'itérable asynchrone à l'aide d'une fonction fournie. Il retourne un nouvel itérable asynchrone avec les valeurs transformées.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const doubledIterable = asyncIterable.map(x => x * 2);

(async () => {
  for await (const value of doubledIterable) {
    console.log(value); // Sortie : 2, 4, 6, 8, 10 (avec un délai de 100ms)
  }
})();

Dans cet exemple, map(x => x * 2) double chaque nombre de la séquence.

filter()

Le helper filter() sélectionne des éléments de l'itérable asynchrone en fonction d'une condition fournie (fonction prédicat). Il retourne un nouvel itérable asynchrone contenant uniquement les éléments qui satisfont la condition.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);

const evenNumbersIterable = asyncIterable.filter(x => x % 2 === 0);

(async () => {
  for await (const value of evenNumbersIterable) {
    console.log(value); // Sortie : 2, 4, 6, 8, 10 (avec un délai de 100ms)
  }
})();

Dans cet exemple, filter(x => x % 2 === 0) sélectionne uniquement les nombres pairs de la séquence.

take()

Le helper take() retourne les N premiers éléments de l'itérable asynchrone. Il retourne un nouvel itérable asynchrone contenant uniquement le nombre spécifié d'éléments.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const firstThreeIterable = asyncIterable.take(3);

(async () => {
  for await (const value of firstThreeIterable) {
    console.log(value); // Sortie : 1, 2, 3 (avec un délai de 100ms)
  }
})();

Dans cet exemple, take(3) sélectionne les trois premiers nombres de la séquence.

drop()

Le helper drop() saute les N premiers éléments de l'itérable asynchrone et retourne le reste. Il retourne un nouvel itérable asynchrone contenant les éléments restants.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const afterFirstTwoIterable = asyncIterable.drop(2);

(async () => {
  for await (const value of afterFirstTwoIterable) {
    console.log(value); // Sortie : 3, 4, 5 (avec un délai de 100ms)
  }
})();

Dans cet exemple, drop(2) saute les deux premiers nombres de la séquence.

toArray()

Le helper toArray() consomme l'intégralité de l'itérable asynchrone et rassemble tous les éléments dans un tableau. Il retourne une promesse qui se résout en un tableau contenant tous les éléments.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const numbersArray = await asyncIterable.toArray();
  console.log(numbersArray); // Sortie : [1, 2, 3, 4, 5]
})();

Dans cet exemple, toArray() rassemble tous les nombres de la séquence dans un tableau.

forEach()

Le helper forEach() exécute une fonction fournie une fois pour chaque élément de l'itérable asynchrone. Il ne retourne *pas* de nouvel itérable asynchrone, il exécute la fonction pour ses effets de bord. Cela peut être utile pour effectuer des opérations comme la journalisation ou la mise à jour d'une interface utilisateur.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(3);

(async () => {
  await asyncIterable.forEach(value => {
    console.log("Valeur :", value);
  });
  console.log("forEach terminé");
})();
// Sortie : Valeur : 1, Valeur : 2, Valeur : 3, forEach terminé

some()

Le helper some() teste si au moins un élément de l'itérable asynchrone passe le test implémenté par la fonction fournie. Il retourne une promesse qui se résout en une valeur booléenne (true si au moins un élément satisfait la condition, false sinon).


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const hasEvenNumber = await asyncIterable.some(x => x % 2 === 0);
  console.log("Contient un nombre pair :", hasEvenNumber); // Sortie : Contient un nombre pair : true
})();

every()

Le helper every() teste si tous les éléments de l'itérable asynchrone passent le test implémenté par la fonction fournie. Il retourne une promesse qui se résout en une valeur booléenne (true si tous les éléments satisfont la condition, false sinon).


async function* generateSequence(end) {
  for (let i = 2; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(4);

(async () => {
  const areAllEven = await asyncIterable.every(x => x % 2 === 0);
  console.log("Sont tous pairs :", areAllEven); // Sortie : Sont tous pairs : true
})();

find()

Le helper find() retourne le premier élément de l'itérable asynchrone qui satisfait la fonction de test fournie. Si aucune valeur ne satisfait la fonction de test, undefined est retourné. Il retourne une promesse qui se résout en l'élément trouvé ou undefined.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const firstEven = await asyncIterable.find(x => x % 2 === 0);
  console.log("Premier nombre pair :", firstEven); // Sortie : Premier nombre pair : 2
})();

reduce()

Le helper reduce() exécute une fonction "réductrice" (reducer) fournie par l'utilisateur sur chaque élément de l'itérable asynchrone, dans l'ordre, en passant la valeur de retour du calcul sur l'élément précédent. Le résultat final de l'exécution du réducteur sur tous les éléments est une valeur unique. Il retourne une promesse qui se résout en la valeur finale accumulée.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const sum = await asyncIterable.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
  console.log("Somme :", sum); // Sortie : Somme : 15
})();

Exemples Pratiques et Cas d'Utilisation

Les Helpers d'Itérateurs Asynchrones sont précieux dans divers scénarios. Explorons quelques exemples pratiques :

1. Traitement de Données d'une API de Streaming

Imaginez que vous construisez un tableau de bord de visualisation de données en temps réel qui reçoit des données d'une API de streaming. L'API envoie des mises à jour en continu, et vous devez traiter ces mises à jour pour afficher les dernières informations.


async function* fetchDataFromAPI(url) {
  let response = await fetch(url);

  if (!response.body) {
    throw new Error("ReadableStream n'est pas supporté dans cet environnement");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      const chunk = decoder.decode(value);
      // En supposant que l'API envoie des objets JSON séparés par des sauts de ligne
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.trim() !== '') {
          yield JSON.parse(line);
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

const apiURL = 'https://example.com/streaming-api'; // Remplacez par l'URL de votre API
const dataStream = fetchDataFromAPI(apiURL);

// Traitement du flux de données
(async () => {
  for await (const data of dataStream.filter(item => item.type === 'metric').map(item => ({ timestamp: item.timestamp, value: item.value }))) {
    console.log('Données traitées :', data);
    // Mettre à jour le tableau de bord avec les données traitées
  }
})();

Dans cet exemple, fetchDataFromAPI récupère les données d'une API de streaming, analyse les objets JSON et les produit sous forme d'itérable asynchrone. Le helper filter sélectionne uniquement les métriques, et le helper map transforme les données dans le format souhaité avant de mettre à jour le tableau de bord.

2. Lecture et Traitement de Gros Fichiers

Supposons que vous deviez traiter un gros fichier CSV contenant des données clients. Au lieu de charger l'intégralité du fichier en mémoire, vous pouvez utiliser les Helpers d'Itérateurs Asynchrones pour le traiter morceau par morceau.


async function* readLinesFromFile(filePath) {
  const file = await fsPromises.open(filePath, 'r');

  try {
    let buffer = Buffer.alloc(1024);
    let fileOffset = 0;
    let remainder = '';

    while (true) {
      const { bytesRead } = await file.read(buffer, 0, buffer.length, fileOffset);
      if (bytesRead === 0) {
        if (remainder) {
          yield remainder;
        }
        break;
      }

      fileOffset += bytesRead;
      const chunk = buffer.toString('utf8', 0, bytesRead);
      const lines = chunk.split('\n');

      lines[0] = remainder + lines[0];
      remainder = lines.pop() || '';

      for (const line of lines) {
        yield line;
      }
    }
  } finally {
    await file.close();
  }
}

const filePath = './customer_data.csv'; // Remplacez par le chemin de votre fichier
const lines = readLinesFromFile(filePath);

// Traitement des lignes
(async () => {
  for await (const customerData of lines.drop(1).map(line => line.split(',')).filter(data => data[2] === 'USA')) {
    console.log('Client des USA :', customerData);
    // Traiter les données des clients des USA
  }
})();

Dans cet exemple, readLinesFromFile lit le fichier ligne par ligne et produit chaque ligne sous forme d'itérable asynchrone. Le helper drop(1) saute la ligne d'en-tête, le helper map divise la ligne en colonnes, et le helper filter sélectionne uniquement les clients des États-Unis.

3. Gestion des Événements en Temps Réel

Les Helpers d'Itérateurs Asynchrones peuvent également être utilisés pour gérer des événements en temps réel provenant de sources comme les WebSockets. Vous pouvez créer un itérable asynchrone qui émet des événements à leur arrivée, puis utiliser les helpers pour traiter ces événements.


async function* createWebSocketStream(url) {
  const ws = new WebSocket(url);

  yield new Promise((resolve, reject) => {
      ws.onopen = () => {
          resolve();
      };
      ws.onerror = (error) => {
          reject(error);
      };
  });

  try {
    while (ws.readyState === WebSocket.OPEN) {
      yield new Promise((resolve, reject) => {
        ws.onmessage = (event) => {
          resolve(JSON.parse(event.data));
        };
        ws.onerror = (error) => {
          reject(error);
        };
        ws.onclose = () => {
           resolve(null); // Résoudre avec null lorsque la connexion se ferme
        }
      });

    }
  } finally {
    ws.close();
  }
}

const websocketURL = 'wss://example.com/events'; // Remplacez par l'URL de votre WebSocket
const eventStream = createWebSocketStream(websocketURL);

// Traitement du flux d'événements
(async () => {
  for await (const event of eventStream.filter(event => event.type === 'user_login').map(event => ({ userId: event.userId, timestamp: event.timestamp }))) {
    console.log('Événement de connexion utilisateur :', event);
    // Traiter l'événement de connexion utilisateur
  }
})();

Dans cet exemple, createWebSocketStream crée un itérable asynchrone qui émet les événements reçus d'un WebSocket. Le helper filter sélectionne uniquement les événements de connexion utilisateur, et le helper map transforme les données dans le format souhaité.

Avantages de l'Utilisation des Helpers d'Itérateurs Asynchrones

Support des Navigateurs et des Environnements d'Exécution

Les Helpers d'Itérateurs Asynchrones sont une fonctionnalité encore relativement nouvelle en JavaScript. Fin 2024, ils sont au stade 3 du processus de standardisation du TC39, ce qui signifie qu'ils seront probablement standardisés dans un avenir proche. Cependant, ils ne sont pas encore pris en charge nativement dans tous les navigateurs et versions de Node.js.

Support des Navigateurs : Les navigateurs modernes comme Chrome, Firefox, Safari et Edge ajoutent progressivement le support pour les Helpers d'Itérateurs Asynchrones. Vous pouvez consulter les dernières informations de compatibilité des navigateurs sur des sites web comme Can I use... pour voir quels navigateurs prennent en charge cette fonctionnalité.

Support de Node.js : Les versions récentes de Node.js (v18 et supérieures) offrent un support expérimental pour les Helpers d'Itérateurs Asynchrones. Pour les utiliser, vous devrez peut-être exécuter Node.js avec le drapeau --experimental-async-iterator.

Polyfills : Si vous devez utiliser les Helpers d'Itérateurs Asynchrones dans des environnements qui ne les prennent pas en charge nativement, vous pouvez utiliser un polyfill. Un polyfill est un morceau de code qui fournit la fonctionnalité manquante. Plusieurs bibliothèques de polyfills sont disponibles pour les Helpers d'Itérateurs Asynchrones ; une option populaire est la bibliothèque core-js.

Implémentation d'Itérateurs Asynchrones Personnalisés

Bien que les Helpers d'Itérateurs Asynchrones offrent un moyen pratique de traiter les itérables asynchrones existants, vous devrez parfois créer vos propres itérateurs asynchrones personnalisés. Cela vous permet de gérer des données provenant de diverses sources, telles que des bases de données, des API ou des systèmes de fichiers, de manière streaming.

Pour créer un itérateur asynchrone personnalisé, vous devez implémenter la méthode @@asyncIterator sur un objet. Cette méthode doit retourner un objet avec une méthode next(). La méthode next() doit retourner une promesse qui se résout en un objet avec les propriétés value et done.

Voici un exemple d'un itérateur asynchrone personnalisé qui récupère des données d'une API paginée :


async function* fetchPaginatedData(baseURL) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseURL}?page=${page}`;
    const response = await fetch(url);
    const data = await response.json();

    if (data.results.length === 0) {
      hasMore = false;
      break;
    }

    for (const item of data.results) {
      yield item;
    }

    page++;
  }
}

const apiBaseURL = 'https://api.example.com/data'; // Remplacez par l'URL de votre API
const paginatedData = fetchPaginatedData(apiBaseURL);

// Traitement des données paginées
(async () => {
  for await (const item of paginatedData) {
    console.log('Élément :', item);
    // Traiter l'élément
  }
})();

Dans cet exemple, fetchPaginatedData récupère les données d'une API paginée, produisant chaque élément au fur et à mesure de sa récupération. L'itérateur asynchrone gère la logique de pagination, ce qui facilite la consommation des données de manière streaming.

Défis Potentiels et Considérations

Bien que les Helpers d'Itérateurs Asynchrones offrent de nombreux avantages, il est important d'être conscient de certains défis et considérations potentiels :

Meilleures Pratiques pour l'Utilisation des Helpers d'Itérateurs Asynchrones

Pour tirer le meilleur parti des Helpers d'Itérateurs Asynchrones, considérez les meilleures pratiques suivantes :

Techniques Avancées

Composition de Helpers Personnalisés

Vous pouvez créer vos propres helpers d'itérateurs asynchrones personnalisés en composant des helpers existants ou en en construisant de nouveaux à partir de zéro. Cela vous permet d'adapter la fonctionnalité à vos besoins spécifiques et de créer des composants réutilisables.


async function* takeWhile(asyncIterable, predicate) {
  for await (const value of asyncIterable) {
    if (!predicate(value)) {
      break;
    }
    yield value;
  }
}

// Exemple d'utilisation :
async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);
const firstFive = takeWhile(asyncIterable, x => x <= 5);

(async () => {
  for await (const value of firstFive) {
    console.log(value);
  }
})();

Combinaison de Plusieurs Itérables Asynchrones

Vous pouvez combiner plusieurs itérables asynchrones en un seul itérable asynchrone en utilisant des techniques comme zip ou merge. Cela vous permet de traiter des données de plusieurs sources simultanément.


async function* zip(asyncIterable1, asyncIterable2) {
    const iterator1 = asyncIterable1[Symbol.asyncIterator]();
    const iterator2 = asyncIterable2[Symbol.asyncIterator]();

    while (true) {
        const result1 = await iterator1.next();
        const result2 = await iterator2.next();

        if (result1.done || result2.done) {
            break;
        }

        yield [result1.value, result2.value];
    }
}

// Exemple d'utilisation :
async function* generateSequence1(end) {
    for (let i = 1; i <= end; i++) {
        yield i;
    }
}

async function* generateSequence2(end) {
    for (let i = 10; i <= end + 9; i++) {
        yield i;
    }
}

const iterable1 = generateSequence1(5);
const iterable2 = generateSequence2(5);

(async () => {
    for await (const [value1, value2] of zip(iterable1, iterable2)) {
        console.log(value1, value2);
    }
})();

Conclusion

Les Helpers d'Itérateurs Asynchrones JavaScript offrent un moyen puissant et élégant de traiter les flux de données asynchrones. Ils proposent une approche fonctionnelle et composable de la manipulation des données, facilitant la construction de pipelines de traitement de données complexes. En comprenant les concepts fondamentaux des Itérateurs Asynchrones et des Itérables Asynchrones et en maîtrisant les différentes méthodes de helper, vous pouvez améliorer considérablement l'efficacité et la maintenabilité de votre code JavaScript asynchrone. Alors que le support des navigateurs et des environnements d'exécution continue de croître, les Helpers d'Itérateurs Asynchrones sont en passe de devenir un outil essentiel pour les développeurs JavaScript modernes.