web-dev-qa-db-fra.com

Evénements vs Streams vs Observables vs Iterators Async

Actuellement, le seul moyen stable de traiter une série de résultats asynchrones en JavaScript est d'utiliser le système d'événements. Cependant, trois alternatives sont en cours d'élaboration:

Flux: https://streams.spec.whatwg.org
Observables: https://tc39.github.io/proposal-observable
Itérateurs asynchrones: https://tc39.github.io/proposal-async-iteration

Quels sont les différences et les avantages de chacun par rapport aux événements et aux autres?

Est-ce que certains d'entre eux ont l'intention de remplacer les événements?

41
Daniel Herr

Il existe à peu près deux catégories d'API: pull et Push.

Tirer

Les API d'extraction asynchrones conviennent parfaitement aux cas où les données sont extraites d'une source. Cette source peut être un fichier, une prise réseau, une liste de répertoires ou toute autre chose. La clé est que le travail est fait pour extraire ou générer des données à partir de la source lorsque demandé.

Les itérateurs asynchrones sont ici la primitive de base, destinée à être une manifestation générique du concept d'une source asynchrone basée sur l'extraction. Dans une telle source, vous:

  • Extraire d'un itérateur asynchrone en faisant const promise = ai.next()
  • Attendez le résultat en utilisant const result = await promise (Ou en utilisant .then())
  • Inspectez le résultat pour savoir s'il s'agit d'une exception (levée), d'une valeur intermédiaire ({ value, done: false }) Ou d'un signal terminé ({ value: undefined, done: true }).

Ceci est similaire à la façon dont les itérateurs de synchronisation sont une manifestation générique du concept d'une source de valeur de synchronisation basée sur l'extraction. Les étapes pour un itérateur de synchronisation sont exactement les mêmes que ci-dessus, en omettant l'étape "attendre le résultat".

Les flux lisibles sont un cas particulier des itérateurs asynchrones, destinés à encapsuler spécifiquement les sources d'E/S comme les sockets/fichiers/etc. Ils ont des API spécialisées pour les diriger vers des flux inscriptibles (représentant l'autre moitié de l'écosystème d'E/S, les puits) et gérer la contre-pression résultante. Ils peuvent également être spécialisés pour gérer les octets d'une manière efficace "apportez votre propre tampon". Tout cela rappelle un peu comment les tableaux sont un cas particulier des itérateurs de synchronisation, optimisés pour O(1) accès indexé).

Une autre caractéristique des API d'extraction est qu'elles sont généralement à consommateur unique. Celui qui tire la valeur, l'a maintenant, et elle n'existe pas dans l'itérateur asynchrone source/stream/etc. plus. Il a été retiré par le consommateur.

En général, les API d'extraction fournissent une interface pour communiquer avec une source de données sous-jacente, permettant au consommateur d'exprimer son intérêt. Cela contraste avec ...

Pousser

Les API push conviennent parfaitement lorsque quelque chose génère des données et que les données générées ne se soucient pas de savoir si quelqu'un le souhaite ou non. Par exemple, peu importe si quelqu'un est intéressé, il est toujours vrai que votre souris a bougé, puis vous avez cliqué quelque part. Vous voudriez manifester ces faits avec une API Push. Ensuite, les consommateurs --- éventuellement plusieurs d'entre eux --- peuvent s'abonner, pour recevoir des notifications poussées sur de telles choses qui se produisent.

L'API elle-même ne se soucie pas de savoir si zéro, un ou plusieurs consommateurs s'abonnent. Il s'agit simplement de manifester un fait sur des choses qui se sont produites dans l'univers.

Les événements en sont une simple manifestation. Vous pouvez vous abonner à un EventTarget dans le navigateur ou à EventEmitter dans Node.js, et être averti des événements qui sont distribués. (Habituellement, mais pas toujours, par le créateur de EventTarget.)

Les observables sont une version plus raffinée d'EventTarget. Leur principale innovation est que l'abonnement lui-même est représenté par un objet de première classe, l'Observable, sur lequel vous pouvez ensuite appliquer des combinateurs (comme un filtre, une carte, etc.). Ils font également le choix de regrouper trois signaux (conventionnellement nommés suivant, complet et erreur) en un seul, et donnent à ces signaux une sémantique spéciale afin que les combinateurs les respectent. C'est par opposition à EventTarget, où les noms d'événement n'ont pas de sémantique spéciale (aucune méthode d'EventTarget ne se soucie de savoir si votre événement est nommé "complet" vs "asdf"). EventEmitter dans Node a une version de cette approche spéciale sémantique où les événements "d'erreur" peuvent planter le processus, mais c'est plutôt primitif.

Une autre caractéristique intéressante des observables sur les événements est que, généralement, seul le créateur de l'observable peut le faire générer les signaux suivants/d'erreur/complets. Alors que sur EventTarget, n'importe qui peut appeler dispatchEvent (). Cette séparation des responsabilités permet un meilleur code, selon mon expérience.

Mais en fin de compte, les événements et les observables sont de bonnes API pour diffuser les événements dans le monde, pour les abonnés qui peuvent se connecter et se déconnecter à tout moment. Je dirais que les observables sont la façon la plus moderne de le faire, et plus agréables à certains égards, mais les événements sont plus répandus et mieux compris. Donc, si quelque chose était destiné à remplacer les événements, ce serait observable.

Poussez <-> tirez

Il convient de noter que vous pouvez créer l'une ou l'autre approche l'une sur l'autre en un clin d'œil:

  • Pour créer Push au-dessus de pull, tirez constamment de l'API pull, puis repoussez les morceaux vers tous les consommateurs.
  • Pour créer un pull au-dessus de Push, abonnez-vous immédiatement à l'API Push, créez un tampon qui accumule tous les résultats et, lorsque quelqu'un tire, récupérez-le dans ce tampon. (Ou attendez que le tampon ne soit pas vide, si votre consommateur tire plus vite que l'API Push encapsulée ne pousse.)

Ce dernier est généralement beaucoup plus de code à écrire que le premier.

Un autre aspect de la tentative d'adaptation entre les deux est que seules les API d'extraction peuvent facilement communiquer la contre-pression. Vous pouvez ajouter un canal latéral aux API Push pour leur permettre de communiquer la contre-pression à la source; Je pense que Dart fait cela, et certaines personnes essaient de créer des évolutions d'observables qui ont cette capacité. Mais c'est IMO beaucoup plus gênant que de choisir correctement une API de traction en premier lieu. Le revers de la médaille est que si vous utilisez une API Push pour exposer une source basée sur l'extraction, vous ne pourrez pas communiquer de contre-pression. C'est d'ailleurs l'erreur commise avec les API WebSocket et XMLHttpRequest.

En général, je trouve que les tentatives d'unification de tout en une seule API en enveloppant les autres sont peu judicieuses. Le push et le pull ont des zones distinctes qui ne se chevauchent pas très bien où chacun fonctionne bien, et dire que nous devrions choisir l'une des quatre API que vous avez mentionnées et la respecter, comme certaines personnes, est à courte vue et conduit à un code maladroit.

113
Domenic

Ma compréhension des itérateurs Async est un peu limitée, mais d'après ce que je comprends, les flux WHATWG sont un cas particulier des itérateurs Async. Pour plus d'informations à ce sujet, reportez-vous à la FAQ sur l'API Streams . Il explique brièvement comment diffère d'Observables .

Les itérateurs asynchrones et les observables sont des moyens génériques de manipuler plusieurs valeurs asynchrones. Pour l'instant, ils n'interagissent pas, mais il semble que la création d'observables à partir d'itérateurs asynchrones soit envisagée. Les observables par leur nature basée sur Push sont beaucoup plus semblables au système d'événement actuel, les AsyncIterables étant basés sur la traction. Une vue simplifiée serait:

-------------------------------------------------------------------------    
|                       | Singular         | Plural                     |
-------------------------------------------------------------------------    
| Spatial  (pull based) | Value            | Iterable<Value>            |    
-------------------------------------------------------------------------    
| Temporal (Push based) | Promise<Value>   | Observable<Value>          |
-------------------------------------------------------------------------    
| Temporal (pull based) | await on Promise | await on Iterable<Promise> |
-------------------------------------------------------------------------    

J'ai représenté AsyncIterables comme Iterable<Promise> pour rendre l'analogie plus facile à raisonner. Notez que await Iterable<Promise> n'a pas de sens car il doit être utilisé dans un for await...of AsyncIterator boucle.

Vous pouvez trouver une explication plus complète ici .

5
kirly