Il s'agit de l'explicateur de la proposition d'API observable pour une manipulation plus ergonomique et composable d'événements.
EventTarget Cette proposition ajoute une méthode .when() à EventTarget qui devient un meilleur addEventListener() ; Plus précisément, il renvoie un nouvel Observable qui ajoute un nouvel écouteur d'événements à la cible lorsque sa méthode subscribe() est appelée. L'observable appelle le gestionnaire next() de l'abonné à chaque événement.
Les observables transforment la gestion, le filtrage et la résiliation des événements en un flux explicite et déclaratif qui est plus facile à comprendre et à composer que la version impérative d'aujourd'hui, qui nécessite souvent des appels imbriqués à addEventListener() et des chaînes de rappel difficiles à suivre.
// Filtering and mapping:
element
. when ( 'click' )
. filter ( ( e ) => e . target . matches ( '.foo' ) )
. map ( ( e ) => ( { x : e . clientX , y : e . clientY } ) )
. subscribe ( { next : handleClickAtPoint } ) ; // Automatic, declarative unsubscription via the takeUntil method:
element . when ( 'mousemove' )
. takeUntil ( document . when ( 'mouseup' ) )
. subscribe ( { next : e => … } ) ;
// Since reduce and some other terminators return promises, they also play
// well with async functions:
await element . when ( 'mousemove' )
. takeUntil ( element . when ( 'mouseup' ) )
. reduce ( ( soFar , e ) => … ) ; // Imperative
const controller = new AbortController ( ) ;
element . addEventListener ( 'mousemove' , e => {
console . log ( e ) ;
element . addEventListener ( 'mouseup' , e => {
controller . abort ( ) ;
} ) ;
} , { signal : controller . signal } ) ; Suivi de tous les clics du lien dans un conteneur (exemple):
container
. when ( 'click' )
. filter ( ( e ) => e . target . closest ( 'a' ) )
. subscribe ( {
next : ( e ) => {
// …
} ,
} ) ; Trouvez la coordonnée Y maximale pendant que la souris est maintenue (exemple):
const maxY = await element
. when ( 'mousemove' )
. takeUntil ( element . when ( 'mouseup' ) )
. map ( ( e ) => e . clientY )
. reduce ( ( soFar , y ) => Math . max ( soFar , y ) , 0 ) ; Multiplexage d'un WebSocket , de sorte qu'un message d'abonnement est envoyé sur une connexion, et un message de désinscription est envoyé au serveur lorsque l'utilisateur se désinscrive.
const socket = new WebSocket ( 'wss://example.com' ) ;
function multiplex ( { startMsg , stopMsg , match } ) {
if ( socket . readyState !== WebSocket . OPEN ) {
return socket
. when ( 'open' )
. flatMap ( ( ) => multiplex ( { startMsg , stopMsg , match } ) ) ;
} else {
socket . send ( JSON . stringify ( startMsg ) ) ;
return socket
. when ( 'message' )
. filter ( match )
. takeUntil ( socket . when ( 'close' ) )
. takeUntil ( socket . when ( 'error' ) )
. map ( ( e ) => JSON . parse ( e . data ) )
. finally ( ( ) => {
socket . send ( JSON . stringify ( stopMsg ) ) ;
} ) ;
}
}
function streamStock ( ticker ) {
return multiplex ( {
startMsg : { ticker , type : 'sub' } ,
stopMsg : { ticker , type : 'unsub' } ,
match : ( data ) => data . ticker === ticker ,
} ) ;
}
const googTrades = streamStock ( 'GOOG' ) ;
const nflxTrades = streamStock ( 'NFLX' ) ;
const googController = new AbortController ( ) ;
googTrades . subscribe ( { next : updateView } , { signal : googController . signal } ) ;
nflxTrades . subscribe ( { next : updateView , ... } ) ;
// And the stream can disconnect later, which
// automatically sends the unsubscription message
// to the server.
googController . abort ( ) ; // Imperative
function multiplex ( { startMsg , stopMsg , match } ) {
const start = ( callback ) => {
const teardowns = [ ] ;
if ( socket . readyState !== WebSocket . OPEN ) {
const openHandler = ( ) => start ( { startMsg , stopMsg , match } ) ( callback ) ;
socket . addEventListener ( 'open' , openHandler ) ;
teardowns . push ( ( ) => {
socket . removeEventListener ( 'open' , openHandler ) ;
} ) ;
} else {
socket . send ( JSON . stringify ( startMsg ) ) ;
const messageHandler = ( e ) => {
const data = JSON . parse ( e . data ) ;
if ( match ( data ) ) {
callback ( data ) ;
}
} ;
socket . addEventListener ( 'message' , messageHandler ) ;
teardowns . push ( ( ) => {
socket . send ( JSON . stringify ( stopMsg ) ) ;
socket . removeEventListener ( 'message' , messageHandler ) ;
} ) ;
}
const finalize = ( ) => {
teardowns . forEach ( ( t ) => t ( ) ) ;
} ;
socket . addEventListener ( 'close' , finalize ) ;
teardowns . push ( ( ) => socket . removeEventListener ( 'close' , finalize ) ) ;
socket . addEventListener ( 'error' , finalize ) ;
teardowns . push ( ( ) => socket . removeEventListener ( 'error' , finalize ) ) ;
return finalize ;
} ;
return start ;
}
function streamStock ( ticker ) {
return multiplex ( {
startMsg : { ticker , type : 'sub' } ,
stopMsg : { ticker , type : 'unsub' } ,
match : ( data ) => data . ticker === ticker ,
} ) ;
}
const googTrades = streamStock ( 'GOOG' ) ;
const nflxTrades = streamStock ( 'NFLX' ) ;
const unsubGoogTrades = googTrades ( updateView ) ;
const unsubNflxTrades = nflxTrades ( updateView ) ;
// And the stream can disconnect later, which
// automatically sends the unsubscription message
// to the server.
unsubGoogTrades ( ) ; Ici, nous tirons parti d'observables pour correspondre à un code secret, qui est un modèle de clés que l'utilisateur peut frapper lors de l'utilisation d'une application:
const pattern = [
'ArrowUp' ,
'ArrowUp' ,
'ArrowDown' ,
'ArrowDown' ,
'ArrowLeft' ,
'ArrowRight' ,
'ArrowLeft' ,
'ArrowRight' ,
'b' ,
'a' ,
'b' ,
'a' ,
'Enter' ,
] ;
const keys = document . when ( 'keydown' ) . map ( e => e . key ) ;
keys
. flatMap ( firstKey => {
if ( firstKey === pattern [ 0 ] ) {
return keys
. take ( pattern . length - 1 )
. every ( ( k , i ) => k === pattern [ i + 1 ] ) ;
}
} )
. filter ( matched => matched )
. subscribe ( ( ) => console . log ( 'Secret code matched!' ) ) ; const pattern = [ ... ] ;
// Imperative
document . addEventListener ( 'keydown' , e => {
const key = e . key ;
if ( key === pattern [ 0 ] ) {
let i = 1 ;
const handler = ( e ) => {
const nextKey = e . key ;
if ( nextKey !== pattern [ i ++ ] ) {
document . removeEventListener ( 'keydown' , handler )
} else if ( pattern . length === i ) {
console . log ( 'Secret code matched!' ) ;
document . removeEventListener ( 'keydown' , handler )
}
} ;
document . addEventListener ( 'keydown' , handler ) ;
}
} , { once : true } ) ;Observable Les observables sont des objets de première classe représentant des événements composables et répétés. Ils sont comme des promesses mais pour plusieurs événements, et spécifiquement avec une intégration EventTarget , ils sont aux événements quelles sont les promesses aux rappels. Ils peuvent être:
subscribe()Observable.map() , à composer et à transformer sans un réseau de rappels imbriqués Mieux encore, la transition des gestionnaires d'événements ➡️ observables est plus simple que celle des rappels ➡️ promet, car les observables s'intègrent bien au-dessus de EventTarget , la manière de facto de s'abonner aux événements de la plate-forme et du script personnalisé. En conséquence, les développeurs peuvent utiliser des observables sans migrer des tonnes de code sur la plate-forme, car il s'agit d'une introduction facile dans les cas où vous gérez les événements aujourd'hui.
La forme de l'API proposée se trouve dans https://wicg.github.io/observable/#core-infrastructure.
Le créateur d'un observable passe dans un rappel qui est invoqué de manière synchrone chaque fois que subscribe() est appelé. La méthode subscribe() peut être appelée n'importe quel nombre de fois , et le rappel qu'il invoque configure un nouvel "abonnement" en enregistrant l'appelant de subscribe() en tant qu'observateur. Avec cela en place, l'observable peut signaler n'importe quel nombre d'événements à l'observateur via le rappel next() , éventuellement suivi d'un seul appel pour complete() ou error() , signalant que le flux de données est terminé.
const observable = new Observable ( ( subscriber ) => {
let i = 0 ;
setInterval ( ( ) => {
if ( i >= 10 ) subscriber . complete ( ) ;
else subscriber . next ( i ++ ) ;
} , 2000 ) ;
} ) ;
observable . subscribe ( {
// Print each value the Observable produces.
next : console . log ,
} ) ; Bien que les observables personnalisés puissent être utiles par eux-mêmes, le cas d'utilisation principal qu'ils déverrouille sont avec la gestion des événements. Les observables renvoyés par la nouvelle méthode EventTarget#when() sont créés nativement avec un rappel interne qui utilise le même mécanisme sous-jacent qu'AdveventListener addEventListener() . Par conséquent, l'appel subscribe() enregistre essentiellement un nouvel auditeur d'événements dont les événements sont exposés via les fonctions du gestionnaire d'observateurs et sont composables avec les différents combinateurs disponibles pour tous les observables.
Les observables peuvent être créés par leur constructeur natif, comme démontré ci-dessus, ou par la méthode statique Observable.from() . Cette méthode construit un natif observable à partir d'objets qui sont l'un des éléments suivants, dans cet ordre :
Observable (auquel cas il renvoie simplement l'objet donné)AsyncIterable (n'importe quoi avec Symbol.asyncIterator )Iterable (n'importe quoi avec Symbol.iterator )Promise (ou tout ce qui est à l'époque) En outre, toute méthode sur la plate-forme qui souhaite accepter un argument observable en tant qu'argument IDL Web, ou en retourner un à partir d'un rappel dont le type de retour est Observable peut également le faire avec l'un des objets ci-dessus, qui sont automatiquement convertis en un observable. Nous pouvons y parvenir de deux manières que nous finaliserons dans la spécification observable:
Observable , un type d'IDL Web spécial qui effectue automatiquement cet objet ECMascript ➡️ La conversion IDL Web, comme le fait Web IDL pour d'autres types.any , et que la prose de spécification correspondante invoque immédiatement un algorithme de conversion que la spécification observable fournira. Ceci est similaire à ce que fait la norme Streams avec Async Iterables aujourd'hui.La conversation dans le n ° 60 se penche vers l'option (1).
Surtout, les observables sont "paresseux" en ce qu'ils ne commencent pas à émettre des données avant qu'ils ne soient abonnés, et ils ne font pas de file d'attente de données avant l'abonnement. Ils peuvent également commencer à émettre des données de manière synchrone pendant l'abonnement, contrairement aux promesses qui filent toujours des microtasses lors de l'invocation .then() gestionnaires. Considérez cet exemple:
el . when ( 'click' ) . subscribe ( { next : ( ) => console . log ( 'One' ) } ) ;
el . when ( 'click' ) . find ( ( ) => { … } ) . then ( ( ) => console . log ( 'Three' ) ) ;
el . click ( ) ;
console . log ( 'Two' ) ;
// Logs "One" "Two" "Three" En utilisant AbortController , vous pouvez vous désinscrire d'un observable même s'il émet de manière synchrone des données pendant l'abonnement:
// An observable that synchronously emits unlimited data during subscription.
let observable = new Observable ( ( subscriber ) => {
let i = 0 ;
while ( true ) {
subscriber . next ( i ++ ) ;
}
} ) ;
let controller = new AbortController ( ) ;
observable . subscribe ( {
next : ( data ) => {
if ( data > 100 ) controller . abort ( ) ;
} } , { signal : controller . signal } ,
} ) ; Il est essentiel qu'un abonné observable puisse enregistrer un rappel de démontage arbitraire pour nettoyer les ressources pertinentes pour l'abonnement. Le démontage peut être enregistré à partir du rappel d'abonnement transmis dans le constructeur Observable . Lors de l'exécution (lors de l'abonnement), le rappel d'abonnement peut enregistrer une fonction de démontage via subscriber.addTeardown() .
Si l'abonné a déjà été interrompu (c'est-à true dire, subscriber.signal.aborted addTeardown() Sinon, il est invoqué de manière synchrone:
complete() , après l'invoquer le gestionnaire complet de l'abonné (le cas échéant)error() , après l'invoquer le gestionnaire d'erreurs de l'abonné (le cas échéant) Nous proposons les opérateurs suivants en plus de l'interface Observable :
catch()Promise#catch() , il faut un rappel qui est licencié après les erreurs observables source. Il mappera ensuite à un nouveau observable, renvoyé par le rappel, à moins que l'erreur ne soit refait.takeUntil(Observable)finally()Promise.finally() , il faut un rappel qui est licencié une fois que l'observable est terminé de quelque manière que ce soit ( complete() / error() ).Observable qui reflète exactement la source observable. Le rappel passé pour finally est licencié lorsqu'un abonnement à l'observable résultant est résilié pour quelque raison que ce soit . Soit immédiatement après la fin de la source, soit des erreurs, soit lorsque le consommateur se désinscrive en interdisant l'abonnement. Les versions de ce qui précède sont souvent présentes dans les implémentations des observables des terres utilisateur car elles sont utiles pour des raisons observables, mais en plus de celles-ci, nous proposons un ensemble d'opérateurs communs qui suivent le précédent de la plate-forme existant et peuvent considérablement augmenter l'utilité et l'adoption. Ceux-ci existent sur d'autres itérables et sont dérivés de la proposition des aides itérateurs de TC39 qui ajoute les méthodes suivantes à Iterator.prototype :
map()filter()take()drop()flatMap()reduce()toArray()forEach()some()every()find() Et la méthode suivante statiquement sur le constructeur Iterator :
from() Nous nous attendons à ce que les bibliothèques d'utilisateurs fournissent plus d'opérateurs de niche qui s'intègrent à l'API Observable central de cette proposition, en expédiant potentiellement nativement s'ils obtiennent suffisamment d'élan pour obtenir leur diplôme sur la plate-forme. Mais pour cette proposition initiale, nous aimerions restreindre l'ensemble des opérateurs à ceux qui suivent le précédent énoncé ci-dessus, similaire à la façon dont les API de plate-forme Web qui sont déclarées Setlike et Maplike ont des propriétés natives inspirées par la carte et les objets de la carte de TC39. Par conséquent, nous considérerions la plupart des discussions sur l'élargissement de cet ensemble comme hors de la proposition initiale , adaptée à la discussion en annexe. Toute longue queue d'opérateurs pourrait éventuellement suivre s'il y a un soutien à l'API observable native présenté dans cet explicateur.
Notez que les opérateurs every() , find() , some() et reduce() renvoient les promesses dont la planification diffère de celle des observables, ce qui signifie parfois des gestionnaires d'événements qui appellent e.preventDefault() fonctionnera trop tard. Voir la section des préoccupations qui va plus en détail.
Pour illustrer comment les observables s'intègrent dans le paysage actuel des autres primitives réactives, consultez le tableau ci-dessous qui est une tentative de combinaison de deux autres tables qui classent les primitives réactives par leur interaction avec les producteurs et les consommateurs:
| Singulier | Pluriel | |||
|---|---|---|---|---|
| Spatial | Temporel | Spatial | Temporel | |
| Pousser | Valeur | Promesse | Observable | |
| Tirer | Fonction | Itérateur asynchrone | Itérable | Itérateur asynchrone |
Les observables ont d'abord été proposés sur la plate-forme de TC39 en mai 2015. La proposition n'a pas réussi à gagner du terrain, en partie en raison de l'opposition que l'API était adaptée à une primitive au niveau du langage. Dans une tentative de renouveler la proposition à un niveau d'abstraction plus élevé, un problème Whatwg Dom a été déposé en décembre 2017. Malgré une grande demande de développeurs, beaucoup de discussions et aucun objectif fort, la proposition Dom Observables a été principalement encore pendant plusieurs années (avec un certain flux dans la conception de l'API) en raison d'un manque de priorisation de l'implémentateur.
Plus tard en 2019, une tentative de relance de la proposition a été faite dans le référentiel TC39 d'origine, qui impliquait des simplifications d'API et a ajouté le soutien du problème synchrone "Firehose".
Ce référentiel est une tentative de respirer à nouveau la vie à la proposition observable dans l'espoir d'en expédier une version sur la plate-forme Web.
Lors de la discussion antérieure, Ben Lesh a énuméré plusieurs implémentations d'utilisateur personnalisées de primitives observables, dont RXJS est la plus populaire auprès de "plus de 47 000 000 téléchargements par semaine ".
start et unsubscribe pour l'observation et l'acquisition de l' Subscription avant le rendement.Actor , pour permettre aux abonnements des changements d'état, comme le montre leur crochet useActor . L'utilisation d'un observable identique est également une partie documentée de l'accès à l'état de modification de la machine lors de l'utilisation de XState avec SolidJS.{ subscribe(callback: (value: T) => void): () => void } motif dans leur routeur et code de différence. Cela a été souligné par les mainteneurs comme étant inspirés par l'observable.{ subscribe(callback: (value: T) => void): () => void } interface pour leurs signaux.{ subscribe(callback: (value: T) => void): () => void } à plusieurs endroits{ subscribe(callback: (value: T) => void): () => void } .{ observe_(callback: (value: T)): () => void } .| async Fonctionnalité | async "Async Pipe" dans les modèles.Compte tenu du vaste art antérieur dans ce domaine, il existe un "contrat observable" public.
De plus, de nombreuses API JavaScript ont essayé d'adhérer au contrat défini par la proposition TC39 de 2015. À cette fin, il y a une bibliothèque, obstacle aux symboles, que des remplaçants de poney (polyfills) Symbol.observable . symbol-observable compte 479 packages dépendants de NPM et est téléchargé plus de 13 000 000 fois par semaine. Cela signifie qu'il y a un minimum de 479 packages sur NPM qui utilisent le contrat observable d'une manière ou d'une autre.
Ceci est similaire à la façon dont les spécifications Promises / A + qui ont été développées avant Promise ont été adoptées en ES2015 en tant que primitive de la langue de première classe.
L'une des principales préoccupations exprimées dans le thread Whatwg Dom original a à voir avec les API prometteuses sur l'observable, comme le first() proposé. Le pied potentiel ici avec la planification des microtasques et l'intégration des événements. Plus précisément, le code d'aspect innocent suivant ne fonctionnerait pas toujours :
element
. when ( 'click' )
. first ( )
. then ( ( e ) => {
e . preventDefault ( ) ;
// Do something custom...
} ) ; Si Observable#first() renvoie une promesse qui résout lorsque le premier événement est licencié sur un EventTarget , alors la promesse appliquée par l'utilisateur .then()
element.click() )e.preventDefault() sera arrivé trop tard et a été effectivement ignoréDans WebIDL, une fois un rappel invoqué, l'algorithme HTML nettoie après le script en cours d'exécution , et ces appels d'algorithme effectuent un point de contrôle de microtasque si et seulement si la pile JavaScript est vide.
Concrètement, cela signifie pour element.click() Dans l'exemple ci-dessus, les étapes suivantes se produisent:
element.click() , un contexte d'exécution JavaScript est d'abord poussé sur la pileclick interne (celui créé nativement par l'implémentation Observable#from() ), un autre contexte d'exécution JavaScript est poussé sur la pile, alors que WebIDL se prépare à exécuter le rappel interneObservable#first() ; Maintenant, la file d'attente Microtask contient le gestionnaire de la promesse sur l'utilisateur de la promesse qui annulera l'événement then() fois qu'il fonctionneraclick interne, le reste du chemin d'événement se poursuit car l'événement n'a pas été annulé pendant ou immédiatement après le rappel. L'événement fait tout ce qu'il ferait normalement (soumettre le formulaire, alert() l'utilisateur, etc.)element.click() est terminé et le contexte d'exécution final est éclaté de la pile et la file d'attente de microtasques est rincé. Le gestionnaire .then() Deux choses atténuent cette préoccupation. Premièrement, il y a une solution de contournement très simple pour toujours éviter le cas où votre e.preventDefault() pourrait fonctionner trop tard:
element
. when ( 'click' )
. map ( ( e ) => ( e . preventDefault ( ) , e ) )
. first ( ) ; ... ou si observable avait une méthode .do() (voir whatswg / dom # 544 (commentaire)):
element
. when ( 'click' )
. do ( ( e ) => e . preventDefault ( ) )
. first ( ) ; ... ou en modifiant la sémantique de first() pour prendre un rappel qui produit une valeur que la promesse retournée résout:
el . when ( 'submit' )
. first ( ( e ) => e . preventDefault ( ) )
. then ( doMoreStuff ) ;Deuxièmement, cette "bizarrerie" existe déjà dans l'écosystème observable florissant d'aujourd'hui, et il n'y a pas de préoccupations ou de rapports sérieux de cette communauté que les développeurs abordent constamment cela. Cela donne une certaine confiance que la cuisson de ce comportement dans la plate-forme Web ne sera pas dangereuse.
Il y a eu beaucoup de discussions sur les normes que le lieu devrait finalement accueillir une proposition d'observables. Le lieu n'est pas sans conséquence, car il décide efficacement si l'observable devient un primitif au niveau du langage comme Promise S, qui expédie dans tous les moteurs de navigateur JavaScript, ou une plate-forme Web primitive avec une considération probable (mais techniquement facultative ) dans d'autres environnements comme Node.js (voir AbortController par exemple).
Les observables intègrent délibérément sans frottement à l'interface émettrice du Main Event ( EventTarget ) et à l'annulation primitive ( AbortController ) qui vivent dans la plate-forme Web. Comme proposé ici, les observables rejoignent ce composant fortement connecté existant à partir de la norme DOM: les observables dépendent d'AbortController / AbortSignal, qui dépendent de EventTarget, et EventTarget dépend à la fois des observables et duCorTroller / AbortSignal. Parce que nous pensons que les observables s'inscrivent le mieux où vivent ses primitives de soutien, le lieu de normes Whatwg est probablement le meilleur endroit pour faire avancer cette proposition. De plus, les incorporations ECMAScript non Web comme Node.js et Deno seraient toujours en mesure d'adopter des observables, et sont même susceptibles de, étant donné leur engagement envers la plate-forme Web et les événements.
Cela n'empêche pas la standardisation future des primitives d'émission d'événements et d'annulation dans TC39 à l'avenir, quelque chose d'observables pourrait théoriquement être superposé en plus ultérieure. Mais pour l'instant, nous sommes motivés à progresser dans Whatwg.
Pour tenter d'éviter de religer cette discussion, nous exhortons le lecteur à voir les commentaires de discussion suivants:
Cette section a mis en place une collection de normes Web et de positions de normes utilisées pour suivre la durée de vie de la proposition observable en dehors de ce référentiel.
Les observables sont conçus pour rendre la gestion des événements plus ergonomique et composable. En tant que tel, leur impact sur les utilisateurs finaux est indirect, venant en grande partie sous la forme d'utilisateurs qui devaient télécharger moins JavaScript pour implémenter des modèles pour lesquels les développeurs utilisent actuellement des bibliothèques tierces. Comme indiqué ci-dessus dans l'explicateur, il existe un écosystème d'observables userland prospère qui entraîne des tas d'octets excessifs téléchargés chaque jour.
Dans une tentative de codifier le fort précédent de l'utilisateur de l'API observable, cette proposition permettrait à des dizaines d'implémentations personnalisées en cours de téléchargement chaque jour.
De plus, en tant qu'API comme EventTarget , AbortController et un lié à Promise S, il permet aux développeurs de créer des flux de gestion des événements moins compliqués en les construisant de manière déclarative, ce qui peut leur permettre de créer des expériences d'utilisateurs plus solides sur le Web.