Este es el explicador de la propuesta API observable para un manejo de eventos más ergonómico y compuesto.
EventTarget Integration Esta propuesta agrega un método .when() a EventTarget que se convierte en un mejor addEventListener() ; Específicamente, devuelve un nuevo Observable que agrega un nuevo oyente de eventos al objetivo cuando se llama a su método subscribe() . El observable llama al controlador next() del suscriptor con cada evento.
Observables convierte el manejo, filtrado y terminación de eventos en un flujo explícito y declarativo que es más fácil de entender y componer que la versión imperativa actual, que a menudo requiere llamadas anidadas a addEventListener() y cadenas de devolución de llamada difíciles de seguir.
// 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 } ) ; Seguimiento de todos los clics del enlace dentro de un contenedor (ejemplo):
container
. when ( 'click' )
. filter ( ( e ) => e . target . closest ( 'a' ) )
. subscribe ( {
next : ( e ) => {
// …
} ,
} ) ; Encuentre la coordenada y máxima y mientras el mouse se mantiene presionado (ejemplo):
const maxY = await element
. when ( 'mousemove' )
. takeUntil ( element . when ( 'mouseup' ) )
. map ( ( e ) => e . clientY )
. reduce ( ( soFar , y ) => Math . max ( soFar , y ) , 0 ) ; Multiplexar un WebSocket , de modo que se envía un mensaje de suscripción a la conexión, y se envía un mensaje de descripción al servidor cuando el usuario se da de baja.
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 ( ) ; Aquí estamos aprovechando los observables para que coincidan con un código secreto, que es un patrón de claves que el usuario podría presionar mientras usa una aplicación:
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 Los observables son objetos de primera clase que representan eventos repetidos compuestos. Son como promesas, pero para múltiples eventos, y específicamente con la integración EventTarget , son a los eventos lo que las promesas son para las devoluciones de llamadas. Pueden ser:
subscribe()Observable.map() , para ser compuesto y transformado sin una red de devoluciones de llamada anidadas Mejor aún, la transición de los manejadores de eventos ➡️ Observables es más simple que la de las devoluciones de llamada ➡️ Promeses, ya que los observables se integran muy bien en la parte superior de EventTarget , la forma de facto de suscribirse a eventos desde la plataforma y el script personalizado. Como resultado, los desarrolladores pueden usar observables sin migrar toneladas de código en la plataforma, ya que es un sometido fácil donde sea que maneje los eventos hoy.
La forma de API propuesta se puede encontrar en https://wicg.github.io/observable/#core-infrastructure.
El creador de un pase observable en una devolución de llamada que se invoca sincrónicamente cada vez que se llama subscribe() . El método subscribe() se puede llamar cualquier número de veces , y la devolución de llamada que invoca establece una nueva "suscripción" registrando la persona que llama de subscribe() como observador. Con esto en su lugar, el observable puede indicar cualquier cantidad de eventos al Observador a través de la devolución de llamada next() , opcionalmente seguida de una sola llamada a complete() o error() , lo que indica que la secuencia de datos está terminada.
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 ,
} ) ; Si bien los observables personalizados pueden ser útiles por su cuenta, el caso de uso principal que desbloquean es con el manejo de eventos. Observables devueltos por el nuevo método EventTarget#when() se crean de forma nativa con una devolución de llamada interna que utiliza el mismo mecanismo subyacente que addEventListener() . Por lo tanto, llamar subscribe() esencialmente registra a un nuevo oyente de eventos cuyos eventos están expuestos a través de las funciones del controlador del observador y se pueden componer con los diversos combinadores disponibles para todos los observables.
Los observables pueden ser creados por su constructor nativo, como se demostró anteriormente, o por el método estático Observable.from() . Este método construye un observable nativo a partir de objetos que son cualquiera de los siguientes, en este orden :
Observable (en cuyo caso solo devuelve el objeto dado)AsyncIterable (cualquier cosa con Symbol.asyncIterator )Iterable (cualquier cosa con Symbol.iterator )Promise (o cualquiera entonces) Además, cualquier método en la plataforma que desee aceptar un observable como un argumento de IDL web, o devolver uno de una devolución de llamada cuyo tipo de retorno sea Observable también puede hacerlo con cualquiera de los objetos anteriores, que se convierten automáticamente en un observable. Podemos lograr esto de una de dos maneras que finalizaremos en la especificación observable:
Observable un tipo de IDL web especial que realiza este objeto ECMAScript ➡️ Conversión de IDL web automáticamente, como lo hace Web IDL para otros tipos.any , y hacer que la prosa de especificación correspondiente invoque inmediatamente un algoritmo de conversión que la especificación observable suministrará. Esto es similar a lo que hace el estándar de Streams con Async Iterables hoy.La conversación en #60 se inclina hacia la opción (1).
Crucialmente, los observables son "perezosos", ya que no comienzan a emitir datos hasta que se suscriban, ni colinan ningún dato antes de la suscripción. También pueden comenzar a emitir datos sincrónicamente durante la suscripción, a diferencia de las promesas que siempre hacen cola las microtasks al invocar los manejadores .then() . Considere este ejemplo:
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" Al usar AbortController , puede darse de baja de un observable incluso cuando emite datos sincrónicamente durante la suscripción:
// 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 } ,
} ) ; Es fundamental que un suscriptor observable pueda registrar una devolución de llamada arbitraria para limpiar cualquier recurso relevante para la suscripción. El desmontaje se puede registrar dentro de la devolución de llamada de suscripción pasada al constructor Observable . Cuando se ejecuta (al suscribirse), la devolución de llamada de suscripción puede registrar una función de desmontaje a través de subscriber.addTeardown() .
Si el suscriptor ya ha sido abortado (es decir, subscriber.signal.aborted es true ), entonces la devolución de llamada dada se invoca inmediatamente desde addTeardown() . De lo contrario, se invoca sincrónicamente:
complete() , después de que se invoca el controlador completo del suscriptor (si lo hay)error() , después de que se invoca el controlador de errores del suscriptor (si lo hay) Proponemos los siguientes operadores además de la interfaz Observable :
catch()Promise#catch() , se necesita una devolución de llamada que se dispara después de los errores observables de la fuente. Luego se asignará a un nuevo observable, devuelto por la devolución de llamada, a menos que el error sea Rethrown.takeUntil(Observable)finally()error() Promise.finally() complete()Observable que refleja la fuente observable exactamente. La devolución de llamada pasada finally se dispara cuando una suscripción al observable resultante se finaliza por cualquier motivo . Inmediatamente después de que la fuente se complete o los errores, o cuando el consumidor se cancela abortando la suscripción. Las versiones de lo anterior a menudo están presentes en las implementaciones de observadores de Userland, ya que son útiles por razones específicas observables, pero además de ellas ofrecemos un conjunto de operadores comunes que siguen el precedente de la plataforma existente y pueden aumentar en gran medida la utilidad y la adopción. Estos existen en otros iterables, y se derivan de la propuesta de ayudantes de Iterator de TC39 que agrega los siguientes métodos al Iterator.prototype :
map()filter()take()drop()flatMap()reduce()toArray()forEach()some()every()find() Y el siguiente método estáticamente en el constructor Iterator :
from() Esperamos que las bibliotecas de Userland proporcionen más operadores de nicho que se integran con la API Observable central de esta propuesta, que potencialmente se envían de forma nativa si obtienen suficiente impulso para graduarse en la plataforma. Pero para esta propuesta inicial, nos gustaría restringir el conjunto de operadores a aquellos que siguen el precedente indicado anteriormente, similar a cómo las API de la plataforma web que se declaran similares a un conjunto y como un mapa tienen propiedades nativas inspiradas en el mapa de TC39 y los objetos establecidos. Por lo tanto, consideraríamos la mayor discusión de expandir este conjunto como fuera del alcance de la propuesta inicial , adecuada para la discusión en un apéndice. Cualquier cola larga de operadores podría seguir si hay apoyo para la API observable nativa presentada en este explicador.
Tenga en cuenta que los operadores every() , find() , some() y reduce() devolver promesas cuya programación difiere de la de los observables, lo que a veces significa que los controladores de eventos que llaman e.preventDefault() se extenderán demasiado tarde. Vea la sección de inquietudes que entra en más detalles.
Para ilustrar cómo los observables se ajustan al paisaje actual de otras primitivas reactivas, consulte la tabla a continuación, que es un intento de combinar otras dos tablas que clasifican las primitivas reactivas por su interacción con los productores y consumidores:
| Singular | Plural | |||
|---|---|---|---|---|
| Espacial | Temporal | Espacial | Temporal | |
| Empujar | Valor | Promesa | Observable | |
| Jalar | Función | Iterador asíncrito | Itreable | Iterador asíncrito |
Los observables se propusieron por primera vez a la plataforma en TC39 en mayo de 2015. La propuesta no logró ganar tracción, en parte debido a cierta oposición de que la API era adecuada para ser una primitiva a nivel de lenguaje. En un intento por renovar la propuesta en un nivel más alto de abstracción, se presentó un problema de WhatWG DOM en diciembre de 2017. A pesar de la amplia demanda de desarrolladores, mucha discusión y no objetores fuertes, la propuesta de Observables DOM se quedó en su mayoría aún durante varios años (con algún flujo en el diseño de la API) debido a la falta de priorización del implementador.
Más tarde, en 2019, se hizo un intento de revivir la propuesta en el repositorio original de TC39, que involucró algunas simplificaciones de API y agregó soporte para el problema sincrónico de "manga de fuego".
Este repositorio es un intento de dar vida nuevamente a la propuesta observable con la esperanza de enviar una versión de TI a la plataforma web.
En una discusión previa, Ben Lesh ha enumerado varias implementaciones personalizadas de usuarios de primitivas observables, de las cuales RXJS es la más popular con "más de 47,000,000 de descargas por semana ".
start y unsubscribe de eventos para observación y adquirir la Subscription antes de la declaración.Actor , para permitir las suscripciones a los cambios en el estado, como se muestra en su gancho useActor . El uso de un observable idéntico también es una parte documentada de los cambios de la máquina de estado de acceso cuando se usa XState con SOLIDJS.{ subscribe(callback: (value: T) => void): () => void } en su código enrutador y diferido. Los mantenedores señalaron esto como inspirados por observables.{ subscribe(callback: (value: T) => void): () => void } para sus señales.{ subscribe(callback: (value: T) => void): () => void } en varios lugares{ subscribe(callback: (value: T) => void): () => void } .{ observe_(callback: (value: T)): () => void } .| async La funcionalidad de "tubería asíncrata" | async en plantillas.Dada la extensa arte anterior en esta área, existe un "contrato observable" público.
Además, muchas API de JavaScript han estado tratando de adherirse al contrato definido por la propuesta TC39 de 2015. Con ese fin, hay una biblioteca, símbolos observables, que los Symbol.observable de pony (polifills). symbol-observable tiene 479 paquetes dependientes en NPM, y se descarga más de 13,000,000 de veces por semana. Esto significa que hay un mínimo de 479 paquetes en NPM que utilizan el contrato observable de alguna manera.
Esto es similar a cómo las promesas/a+ especificaciones desarrolladas antes de Promise se adoptaron en ES2015 como un lenguaje de primera clase primitivo.
Una de las principales preocupaciones expresadas en el hilo original de Whatwg DOM tiene que ver con las API prometedoras para observables, como el propuesto first() . El potencial pistola de fútbol aquí con programación de microtask e integración de eventos. Específicamente, el siguiente código de aspecto inocente no siempre funcionaría:
element
. when ( 'click' )
. first ( )
. then ( ( e ) => {
e . preventDefault ( ) ;
// Do something custom...
} ) ; Si Observable#first() devuelve una promesa que se resuelve cuando el primer evento se dispara en un EventTarget , entonces la promesa suplicada por el usuario .then() Handler se ejecutará:
element.click() )e.preventDefault() habrá sucedido demasiado tarde y efectivamente ha sido ignoradoEn WebIDL después de invoca una devolución de llamada, se llama la limpieza del algoritmo HTML después de ejecutar el script , y este algoritmo las llamas a un punto de control de MicroTask si y solo si la pila JavaScript está vacía.
Concretamente, eso significa para element.click() En el ejemplo anterior, ocurren los siguientes pasos:
element.click() , primero se empuja un contexto de ejecución de JavaScript a la pilaclick interno (la creada de forma nativa por la implementación Observable#from() ), otro contexto de ejecución de JavaScript se empuja a la pila, mientras WebIDL se prepara para ejecutar la devolución de llamada internaObservable#first() ; Ahora la cola MicroTask contiene el controlador then() con suministro de usuario de la promesa que cancelará el evento una vez que se ejecuteclick interno, el resto de la ruta del evento continúa ya que el evento no se canceló durante o inmediatamente después de la devolución de llamada. El evento hace lo que sea que haga normalmente (envíe el formulario, alert() al usuario, etc.)element.click() Se ejecuta el controlador .then() suministrado por el usuario, que intenta cancelar el evento demasiado tarde Dos cosas mitigan esta preocupación. Primero, hay una solución muy simple para evitar siempre el caso en el que su e.preventDefault() pueda correr demasiado tarde:
element
. when ( 'click' )
. map ( ( e ) => ( e . preventDefault ( ) , e ) )
. first ( ) ; ... o si observable tenía un método .do() (ver Whatwg/DOM#544 (comentario)):
element
. when ( 'click' )
. do ( ( e ) => e . preventDefault ( ) )
. first ( ) ; ... o modificando la semántica de first() para tomar una devolución de llamada que produce un valor que la promesa devuelta resuelve:
el . when ( 'submit' )
. first ( ( e ) => e . preventDefault ( ) )
. then ( doMoreStuff ) ;En segundo lugar, este "peculiar" ya existe en el próspero ecosistema observable de hoy, y no hay preocupaciones o informes serios de esa comunidad de que los desarrolladores se encuentran constantemente con esto. Esto le da cierta confianza a que hornear este comportamiento en la plataforma web no será peligroso.
Ha habido mucha discusión sobre qué lugar de estándares debería organizar una propuesta de observables. El lugar no es intrascendente, ya que decide efectivamente si los observables se convierten en una promesa primitiva a nivel de lenguaje como Promise , que se envían en todos los motores de navegador JavaScript, o una plataforma web primitiva con consideración probable (pero técnicamente opcional ) en otros entornos como Node.js (ver AbortController por ejemplo).
Observables integran a propósito sin fricción con la interfaz de emisión de eventos principales ( EventTarget ) y la cancelación primitiva ( AbortController ) que viven en la plataforma web. Como se propone aquí, los observables se unen a este componente fuertemente conectado existente desde el estándar DOM: los observables dependen de AbortController/Abortsignal, que dependen de EventTarget, y EventTarget depende de Observables y AbortController/Abortsignal. Debido a que sentimos que los observables se ajustan en el mejor de los que viven sus primitivas de apoyo, el lugar de estándares WhatWG es probablemente el mejor lugar para avanzar en esta propuesta. Además, los integrantes de Ecmascript no Web como Node.js y Deno aún podrían adoptar observables, e incluso es probable que lo hagan, dado su compromiso con el aborto y los eventos de la plataforma web.
Esto no impide la estandarización futura de las primitivas de emisión de eventos y de cancelación en TC39 en el futuro, algo observable podría ser en cola en cola además de más adelante. Pero por ahora, estamos motivados para avanzar en Whatwg.
En un intento de evitar la relación con esta discusión, instamos al lector a ver los siguientes comentarios de discusión:
Esta sección prevé una colección de estándares web y posiciones de estándares que se utilizan para rastrear la vida de la propuesta observable fuera de este repositorio.
Los observables están diseñados para hacer que el manejo de eventos sea más ergonómico y compuesto. Como tal, su impacto en los usuarios finales es indirecto, en gran medida en forma de usuarios que tienen que descargar menos JavaScript para implementar patrones para los que los desarrolladores usan actualmente bibliotecas de terceros. Como se indicó anteriormente en el explicador, hay un próspero ecosistema de observaciones de usuarios que da como resultado un montón de bytes excesivos que se descargan todos los días.
En un intento por codificar el precedente de la API observable de la tierra de usuario, esta propuesta evitaría que se descarguen docenas de implementaciones personalizadas todos los días.
Además, como una API como EventTarget , AbortController y una relacionada con Promise , permite a los desarrolladores construir flujos de manejo de eventos menos complicados mediante la construcción de declaraciones declarativas, lo que puede permitirles construir más experiencias de usuario sólidas en la Web.