Fermer

novembre 29, 2021

Comparer les producteurs de données en JavaScript


Les fonctions, les promesses, les itérables et les observables sont les producteurs de JavaScript. Chacun peut produire une valeur/séquence de valeurs et l'envoyer aux consommateurs.

chatons dans un panier" title="chatons dans un panier"/></p>
<p><em>Crédit photo : <a href=Jari Hytönen sur Unsplash.

Producteurs et consommateurs de données

Nos applications contiennent souvent du code qui produit des données et du code qui utilise ces données. Le code responsable de la production de données est appelé producteur ou source de donnéestandis que le code qui consomme les données est appelé consommateur.

Un producteur encapsule le code pour produire des données et fournit les moyens de communiquer avec le consommateur. Un producteur peut produire Il peut obtenir les données en les récupérant à partir d'une API, en écoutant des événements DOM, en effectuant un calcul basé sur des valeurs d'entrée ou même en stockant des données codées en dur.

Le diagramme ci-dessous illustre que les producteurs varient quand et quand et comment ils produisent des données ainsi que comment ils envoient des données au consommateur.

Producteur (produit des données) avec une flèche vers Consommateur (consomme des données). La flèche contient : push/pull, paresseux/avide, valeur unique/séquence de valeurs, synchrone/asynchrone, unicast/multicast

Icônes réalisées par Freepik à partir de www.flaticon. com.

Un producteur peut :

  • avoir un système pull ou push
  • avoir lazy ou une exécution impatiente
  • retourner une valeur unique ou émettre une séquence de valeurs
  • effectuer une synchrone ou une asynchrone opération pour produire des données
  • unicast ou multicast données aux consommateurs

Devinez quels producteurs sont disponibles en JavaScript ?

Producteurs en JavaScript

Les fonctions, les promesses, les itérables et les observables sont les producteurs de JavaScript. Chacune peut produire une valeur, ou dans certains cas une séquence de valeurs, et l'envoyer aux consommateurs.

Les fonctions et les promesses renvoient toutes deux une valeur unique. Cependant, les fonctions sont synchrones et paresseuses, alors que les promesses sont asynchrones et impatientes.

Les itérables et les observables nous permettent de travailler avec des séquences de données (également appelées flux de données). Cependant, les itérables sont synchrones et paresseux, tandis que les observables peuvent produire des données de manière synchrone ou asynchrone.

Les fonctions, les promesses et les itérables sont intégrés à JavaScript. Alors que les observables ne font pas encore partie de JavaScript et sont implémentés par des bibliothèques telles que RxJS.

Regardons chacun de plus près.

Functions

Les fonctions produisent une seule valeur. Une fonction prend une entrée, effectue une opération sur l'entrée et renvoie une valeur unique en sortie. Si le corps de la fonction n'a pas d'instruction return pour renvoyer une valeur, il renvoie implicitement undefined.

function sumNaturalNumbers(num) {
  if (num <= 1) {
    return num;
   }
  return sumNaturalNumbers(num - 1) + num;
}

Fonctions exécuté paresseusement. Nous n'obtiendrons aucune donnée de notre déclaration de fonction ci-dessus car les fonctions sont inertes. La déclaration de fonction ne définit que les paramètres et dit quoi faire dans le corps. Le code dans le corps de la fonction n'est pas exécuté tant que nous n'appelons pas la fonction et ne transmettons aucun argument. La fonction ne retournera une valeur que lorsque nous le lui demanderons, c'est pourquoi nous l'appelons paresseux. Les fonctions sont exécutées paresseusement ou à la demande.

L'appelant (consommateur) contrôle le moment où il reçoit des données d'une fonction. Ils extraient les données de la fonction.

Notre fonction sumNaturalNumbers() n'est exécutée que lorsque nous l'appelons :

sumNaturalNumbers(10) ;

Les fonctions sont synchrones. Lorsque nous appelons une fonction, le moteur JavaScript crée un contexte d'exécution de fonction contenant les arguments et les variables locales de la fonction et l'ajoute à la pile d'appels JavaScript.

Le moteur JavaScript exécute chaque ligne de code dans le corps de la fonction jusqu'à ce que la fonction revienne. Ensuite, le moteur JavaScript supprime le contexte d'exécution de la fonction de la pile d'appels JavaScript.

Les appels de fonction (à l'exception des rappels asynchrones) s'exécutent directement sur le thread principal du processus de rendu du navigateur. Le fil principal du processus de rendu est responsable de l'exécution du JavaScript de notre application Web. Le code synchrone de notre application s'exécute directement sur le thread principal – il est ajouté en haut de la pile d'appels (sans attendre que la pile d'appels soit vide au préalable).

Alors que les rappels asynchrones doivent d'abord attendre dans une file d'attente avant de pouvoir s'exécuter. sur le fil principal. Nous utilisons des API Web pour effectuer des tâches asynchrones dans nos applications. Par exemple, pour récupérer des données du réseau ou exécuter des opérations gourmandes en CPU sur les threads de travail. Nous traitons les résultats de ces tâches dans notre application via des fonctions de rappel et des gestionnaires d'événements.

Une fois la tâche asynchrone terminée, le thread effectuant la tâche asynchrone met en file d'attente le rappel vers une file d'attente de tâches ou une file d'attente de microtâches. La boucle d'événement exécute les rappels en file d'attente sur le thread principal lorsque la pile d'appels JavaScript est vide.

Super, regardons ensuite les itérables. 🌼🌸🌺

Iterables

Iterables ont été introduits dans JavaScript dans ES2015. Un objet est itérable s'il a une méthode Symbol.iterator qui renvoie un objet itérateur.

L'objet itérateur a une méthode appelée next() qui nous permet d'itérer sur les valeurs dans l'itérable.

L'appel d'un iterator.next() renvoie un objet avec deux propriétés :

  • value est la valeur suivante dans la séquence d'itération
  • done est true s'il ne reste plus de valeurs dans la séquence

Créons un itérateur pour itérer sur un itérable.

Les fonctions génératrices facilitent la création d'un itérable et son itérateur. 🦋 Le mot-clé function suivi d'un astérisque (function*) définit une fonction génératrice.

On peut considérer le mot-clé yield comme des retours intermédiaires. En utilisant yieldnous pouvons renvoyer plusieurs valeurs avant d'atteindre l'instruction return finale.

function* generateVowelsIterator() {   
    rendement 'a';
    rendement 'e';
    rendement 'i';
    rendement ' o';
    yield 'u';  
    return true;
}

Pour consommer les données de la fonction génératrice, nous demandons un iterator—l'appel d'une fonction génératrice renvoie un itérateur :

const voyelsIterator = generateVowelsIterator();  

Nous pouvons maintenant appeler next( ) sur l'itérateur. Cela demande à la fonction génératrice d'évaluer la première expression yield et de renvoyer la valeur. Chaque fois que nous appelons iterator.next()la fonction génératrice évalue la prochaine instruction yield et renvoie la valeur, jusqu'à ce que la fonction renvoie la valeur finale et définisse done à true.

voyellesIterator.next();
voyellesItérateur.suivant();
voyellesItérateur.suivant();
voyellesItérateur.suivant();
voyellesItérateur.suivant();
voyellesIterator.next(); 

Comme les fonctions, les fonctions génératrices peuvent accepter des paramètres, donc au lieu de coder en dur les valeurs obtenues, nous pouvons créer un itérateur plus générique :

function* generateWordIterator(word) {  
  let count = 0;[19659130]pour (let i = 0; i < mot.longueur ; i++) {
    count++;  
    yield i;  
  }  
  return count;  
}

Nous n'avons pas réellement besoin de créer des itérateurs personnalisés pour itérer sur les valeurs d'une chaîne. Très commodément pour nous, dans ES6, les collections sont devenues itérables. Ainsi, les types string, array, map et set sont des itérables intégrés dans JavaScript. Chacun de ces types a une méthode Symbol.iterator dans sa chaîne de prototypes qui renvoie son itérateur.

Reprenons alors notre exemple de voyelles. Nous pouvons stocker les voyelles dans une chaîne et la parcourir en utilisant l'instruction for...of  :

const voyelles = 'aeiou';[19659152]pour (let voyelle de voyelles) {
  console.log(voyelle);  
}

Nous utilisons souvent l'instruction for...ofla opérateur de propagation [...'abc'] et affectations de déstructuration [a,b,c]=['a', 'b', 'c'] pour itérer sur les valeurs. Dans les coulisses, ils demandent à l'itérable d'un objet itérateur d'itérer sur leurs valeurs.

Maintenant que nous avons examiné des exemples d'itérateurs, comment se comparent-ils aux fonctions ?

Tout comme les fonctions, les itérateurs sont ]paresseux et synchrone. Contrairement aux fonctions, un itérable peut renvoyer plusieurs valeurs au fil du temps via son itérateur. Nous pouvons continuer à appeler iterator.next() pour obtenir la valeur suivante de la séquence jusqu'à ce que la séquence soit consommée.

Regardons ensuite les promesses. 🎁

Promises

Un objet Promise représente l'achèvement éventuel (ou l'échec) d'une opération asynchrone et sa valeur résultante (ou erreur).

const myPromise =  nouveau Promesse((résolution, rejeter) => {
    
    setTimeout([19659027]() => {  
      résolution('une certaine valeur');  
  },  1000);  
})

Nous passons les gestionnaires de succès à une promesse en appelant sa méthode then(). De même, nous passons un gestionnaire d'erreurs à une promesse en appelant sa méthode catch().

(Nous pourrions passer des gestionnaires d'erreurs comme deuxième paramètre à la méthode then() ainsi, cependant, il est plus courant de laisser la gestion des erreurs à la méthode catch().)

myPromise
  .puis(successHandlerA)  
  .puis(successHandlerB)  
  .attrape ](errorHandler);

Un objet de promesse a deux propriétés :

  • status—comme son nom l'indique, status stocke le statut de la promesse (en attente, remplie ou rejetée)[19659012]valeur : la valeur renvoyée par l'opération asynchrone

Alors que l'opération asynchrone est toujours en cours, la promesse est en attente et la valeur n'est pas définie.

Si l'opération se termine avec succès, l'objet de la promesse :

  • met à jour son La propriété state à fulfilled
  • définit sa valeur sur la valeur renvoyée par l'opération asynchrone
  • ajoute les rappels de succès avec la valeur promise à la file d'attente de microtâches

D'un autre côté, si l'opération asynchrone a une erreur, la promesse ob ject :

  • met à jour son état à rejected
  • définit sa valeur sur les informations d'erreur
  • ajoute le rappel d'erreur à la file d'attente de microtâches avec les informations d'erreur

En bref, une promesse soit se résout en une valeur lorsque l'opération asynchrone est terminée avec succès, ou elle se résout avec une motif d'erreur si l'opération échoue.

Les promesses sont toujours asynchrone car ils ajoutent le rappel de réussite ou d'erreur à la file d'attente des microtâches. La boucle d'événement exécute les rappels en file d'attente lorsque la pile d'appels JavaScript est vide.

Contrairement aux fonctions et aux itérables, les promesses ne sont pas paresseuses, mais avides . Une promesse en JavaScript représente une action asynchrone qui a déjà été lancée. Par exemple, l'appel de fetch() démarre l'opération asynchrone de demande de la ressource spécifiée auprès du réseau et renvoie la promesse qui représente cette opération.

const pikachuPromise = 
 chercher('https://pokeapi.co/api/v2/pokemon/pikachu');

pikachuPromesse
  .puis(réponse => réponse.json())
  .[19659100]puis(data => console.log(data))
  .[19659135]catch(err => console.error(err));

Les promesses sont multidiffusion. Les rappels seront invoqués même s'ils ont été ajoutés après le succès ou l'échec de l'opération asynchrone que la promesse représente.

Regardons ensuite les observables 🕶 et voyons comment ils se comparent aux promesses, itérables et fonctions.

Observables

Un observable représente une séquence de valeurs qui peuvent être observées. — TC39

Les observables sont des collections Push paresseuses de plusieurs valeurs. — RxJS

Les observables remplissent la place manquante pour un producteur en JavaScript qui peut envoyer une séquence de valeurs de manière asynchrone. Ceci est illustré dans le tableau suivant :

PullFunctionIterator
PushPromiseObservable

Observables fournissent une manière unifiée de travailler avec différents types de données. Ils peuvent produire :

  • Une valeur unique (comme des fonctions et des promesses) ou plusieurs valeurs (comme des itérables)
  • De manière synchrone (comme des fonctions et des itérables) ou de manière asynchrone (comme des promesses)
  • Paresseusement (observable à froid) ou avec impatience ( observable à chaud)
  • Unidiffusion à un seul consommateur (observable à froid) ou multidiffusion à plusieurs consommateurs (observable à chaud)

Contrairement aux promesses et aux protocoles d'itérationles observables ne font pas encore partie de JavaScript. Cependant, il existe une proposition TC39 pour ajouter un type observable à JavaScript. Nous pouvons utiliser des bibliothèques qui implémentent le type observable, dont le plus populaire est RxJS (avec 24 895 323 téléchargements hebdomadaires npm au moment de la rédaction).

L'astuce pour comprendre les observables consiste à voir comment une instance observable est créée.

Nous passons une fonction d'abonné au constructeur observable.

La fonction d'abonné prend un observateur comme paramètre d'entrée. Un observateur est un objet avec des propriétés qui contiennent les rappels suivant, d'erreur et complet. rappeler. De même, nous informons l'observateur d'une erreur en appelant le rappel error() et de l'achèvement en appelant le rappel complete().

import { Observable } from 'rxjs';

const myObservable$ = nouveau Observable(abonné);

fonction abonné(observateur) {  
  

  
  
  
  
  
  retour () =>  {
    
  };
}

Pour consommer les données de l'observable, nous devons d'abord souscrire à l'instance observable en appelant la méthode de souscription et en passant un observateur. L'abonnement à l'instance observable exécute la fonction d'abonné, qui produit des données et appelle les rappels appropriés lorsqu'elle a des données, qu'une erreur se produit ou qu'elle est terminée.

myObservable$.subscribe([19659027]{
  suivant: (données) =>
  erreur: (erreur) =>
  complete: () => 
});

Cependant, nous n'avons généralement pas besoin de définir la logique pour créer une instance observable nous-mêmes. La bibliothèque RxJS fournit des fonctions de création observables pour les cas d'utilisation courants, tels que offromEventintervalconcat et bien d'autres.[19659306]Pull vs. Push Systems

Pull

Dans un système pull, le consommateur extrait les données du producteur. Le consommateur a le contrôle et il décide quand obtenir les données – il extrait les données du producteur quand il le souhaite. devoir attendre et sans bloquer.

Le thread principal du processus de rendu est responsable de :

  • rendant la page Web
  • répondant aux entrées de l'utilisateur
  • ainsi que d'exécuter le JavaScript de l'application

Le principal thread ne peut faire qu'une tâche à la fois. Par conséquent, si une fonction met trop de temps à revenir, pendant son exécution, la fonction bloque le thread principal et l'empêche de rendre la page et de répondre aux entrées de l'utilisateur.

Exemples

Deux des producteurs en JavaScript ont un système d'extraction :

  1. Fonctions

Comme indiqué dans le code ci-dessous, nous tirons la valeur d'une fonction en appelant la fonction.

function sum(a, b) {  
  retour a + b;  
}
const coût = sum(1, 2);
  1. Iterables

Dans le code ci-dessous, nous tirons les valeurs du tableau (qui est un itérable) à l'aide d'une affectation déstructurante. L'affectation de déstructuration utilise l'itérateur intégré du tableau pour parcourir les éléments du tableau colorPalette et affecter la valeur aux variables correspondantes royalblueetc. spécifiées dans la déstructuration du tableau.[19659025]const colorPalette = ['hsl(216,87%,48%)', 'hsl(216,87%,48%)'[19659027], 'hsl(42,99%,52%)', 'hsl(7,66%,49%)'];

const[19659028][royalblue, seagreen, orange, firebrick] = colorPalette;

Push

Dans un système push, le producteur envoie des données au consommateur lorsque les données sont disponibles.

Le consommateur fait savoir au producteur qu'il souhaite recevoir des données. Cependant, le consommateur ne sait pas quand les données arriveront. Par exemple, si le consommateur a demandé au producteur des données qui doivent être récupérées du réseau, des facteurs tels que la connectivité du réseau affectent le temps qu'il faut au producteur pour recevoir des données.

Le consommateur ne veut pas bloquer le thread de rendu pendant qu'il attend les données du producteur. Il ne veut pas non plus continuer à vérifier auprès du producteur si les données sont déjà disponibles. Que peut faire le consommateur à la place ? Il peut envoyer un rappel au producteur !

Fonctions de rappel

Le consommateur peut définir une fonction qui accepte les données en entrée et implémente la logique pour traiter les données. Il peut envoyer cette fonction au producteur. Une telle fonction est appelée un rappel. Lorsque le producteur a les données disponibles, il peut appeler la fonction de rappel, en passant les données en argument.

De plus, le consommateur peut envoyer des fonctions de rappel pour gérer les erreurs et un rappel pour être averti que le producteur a fini d'envoyer tous les données (si le producteur le permet).

Les promesses et les observables sont tous deux des exemples d'un système push. Nous avons déjà rencontré les rappels qu'ils acceptent :

Pour traiter les donnéesthen()next()
To handle errorcatch()error()
To handle complétioncomplete()

Le système push est vraiment bien adapté pour traiter des données asynchrones. Le consommateur n'a pas à attendre les données, il transmet simplement ses rappels au producteur qui exécutera le rappel approprié lorsqu'il sera prêt.

Cela dit, les observables peuvent produire et émettre des données de manière synchrone et asynchrone.[19659006] Les promesses mettent en file d'attente les rappels dans une microtâche pour que la boucle d'événements s'exécute. Observable qui effectue une opération asynchrone pour obtenir des données en file d'attente les rappels dans une file d'attente de tâches pour que la boucle d'événements s'exécute.

Bien que les promesses et les observables soient tous deux des systèmes push, ils présentent de nombreuses distinctions. Les promesses sont toujours multicastasynchroneavide et se résolvent en une valeur unique. Alors que les observables peuvent être unicast ou multicastsynchrones ou asynchronesrenvoyer une valeur unique ou plusieurset sont paresseux s'ils sont froids et impatients s'ils sont chauds .

Maintenant que nous avons vu que les observables et les promesses sont tous deux des systèmes push, voyons ensuite ce que les observables ont en commun avec les itérables.

Data Streams—The Iterator and Observer Design Patterns

Iterables et les observables traitent tous deux de flux de données. Au lieu de renvoyer une seule valeur au consommateur, les itérables et les observables peuvent envoyer une séquence de valeurs. La séquence peut contenir zéro ou plusieurs valeurs. Logiciel orienté objet réutilisable . "

Modèle de conception d'itérateur

Le modèle d'itérateur décrit la sémantique pour un client (consommateur) d'itérer sur une séquence de valeurs (l'itérable). Le modèle d'itérateur comprend une sémantique pour l'erreur et l'achèvement. Il décrit une relation pull entre le producteur et le consommateur.

Les protocoles iterable et iterator ont été ajoutés à ECMAScript 2015.

Le modèle d'itérateur est un modèle de conception dans lequel un itérateur est utilisé pour parcourir un conteneur et accéder aux éléments du conteneur. Le modèle d'itérateur découple les algorithmes des conteneurs ; dans certains cas, les algorithmes sont nécessairement spécifiques au conteneur et ne peuvent donc pas être découplés. — Wikipedia

Observer Design Pattern

Le modèle d'observateur fait la même chose que l'itérateur mais dans la direction opposée. Il décrit une relation push entre le producteur et le consommateur.

Les observables ne font pas encore partie d'ECMAScript (cependant, il existe une proposition du TC39 pour ajouter des observables à ECMAScript). Nous pouvons utiliser des observables via la bibliothèque RxJS.

Bien que le modèle d'observateur décrit par le Gang of Four n'inclue pas la sémantique pour l'achèvement, des gens intelligents de la communauté JavaScript ont réalisé la puissance d'un système basé sur le push qui notifie le consommateur de achèvement. J'aime beaucoup les discours de Jafar Husain qui explique cela magnifiquement. Par exemple, dans cette conférence Jafar démontre à quel point il est facile de créer une collection de déplacements de souris à l'aide d'observables, car les observables peuvent informer leurs abonnés lorsqu'ils ont terminé de produire des données.

Le modèle d'observateur est une conception logicielle. modèle dans lequel un objet, nommé le sujet, maintient une liste de ses dépendants, appelés observateurs, et les notifie automatiquement de tout changement d'état, généralement en appelant l'une de leurs méthodes. — Wikipedia

Résumé

Le tableau ci-dessous présente un résumé simple et agréable de ce que nous avons couvert dans cet article :

FonctionValeur unique, synchrone, paresseux, pull
PromiseValeur unique, asynchrone, avide, pull
IterableValeurs multiples, synchrone, paresseux, push
ObservableValeurs multiples, synchrones ou asynchrones, paresseux ou avides, push

Autres ressources






Source link