Este é o explicador para a proposta de API observável para manuseio de eventos mais ergonômico e composível.
EventTarget Esta proposta adiciona um método .when() ao EventTarget que se torna um melhor addEventListener() ; Especificamente, ele retorna um novo Observable que adiciona um novo ouvinte de eventos ao destino quando o método subscribe() é chamado. O observável chama o manipulador next() do assinante em cada evento.
Observáveis transformam o manuseio, filtragem e rescisão de eventos, em um fluxo declarativo explícito, mais fácil de entender e compor do que a versão imperativa de hoje, que geralmente requer chamadas aninhadas para as cadeias de retorno de chamada addEventListener() e de retorno difícil 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 } ) ; Rastreando todos os cliques de link dentro de um contêiner (exemplo):
container
. when ( 'click' )
. filter ( ( e ) => e . target . closest ( 'a' ) )
. subscribe ( {
next : ( e ) => {
// …
} ,
} ) ; Encontre a coordenada máxima y enquanto o mouse é retido (exemplo):
const maxY = await element
. when ( 'mousemove' )
. takeUntil ( element . when ( 'mouseup' ) )
. map ( ( e ) => e . clientY )
. reduce ( ( soFar , y ) => Math . max ( soFar , y ) , 0 ) ; Multiplinar um WebSocket , de modo que uma mensagem de assinatura seja enviada na conexão e uma mensagem de cancelamento de inscrição seja enviada ao servidor quando o usuário cancela inscrições.
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 ( ) ; Aqui estamos aproveitando os observáveis para combinar com um código secreto, que é um padrão de chaves que o usuário pode atingir ao usar um aplicativo:
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 Observáveis são objetos de primeira classe representando eventos repetidos e compostos. Eles são como promessas, mas para vários eventos e, especificamente, com a integração EventTarget , são para eventos o que as promessas são para retornos de chamada. Eles podem ser:
subscribe()Observable.map() , a ser composto e transformado sem uma rede de retornos de chamada aninhados Melhor ainda, a transição dos manipuladores de eventos ➡️ Observables é mais simples que a das promessas de chamada, uma vez que os observáveis se integram bem no topo do EventTarget , a maneira de fato de se inscrever em eventos da plataforma e script personalizado. Como resultado, os desenvolvedores podem usar observáveis sem migrar toneladas de código na plataforma, pois é uma queda fácil onde quer que você esteja lidando com eventos hoje.
A forma da API proposta pode ser encontrada em https://wicg.github.io/observable/#core-infrastructure.
O criador de um observável passa em um retorno de chamada que é invocado de maneira síncrona sempre que subscribe() é chamada. O método subscribe() pode ser chamado de várias vezes , e o retorno de chamada que ele chama configura uma nova "assinatura" registrando o chamador de subscribe() como um observador. Com isso em vigor, o observável pode sinalizar qualquer número de eventos para o observador por meio de retorno de chamada next() , seguido opcionalmente por uma única chamada para complete() ou error() , sinalizando que o fluxo de dados está concluído.
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 ,
} ) ; Embora os observáveis personalizados possam ser úteis por conta própria, o principal caso de uso que eles desbloqueiam é com o manuseio de eventos. Observáveis retornados pelo novo método EventTarget#when() são criados nativamente com um retorno de chamada interno que usa o mesmo mecanismo subjacente que addEventListener() . Portanto, a chamada de subscribe() registra essencialmente um novo ouvinte de eventos cujos eventos são expostos através das funções do manipulador do Observer e são composíveis com os vários combinadores disponíveis para todos os observáveis.
Observáveis podem ser criados por seu construtor nativo, como demonstrado acima ou pelo método estático Observable.from() . Este método constrói um observável nativo a partir de objetos que são qualquer um dos seguintes, nesta ordem :
Observable (nesse caso, apenas retorna o objeto especificado)AsyncIterable (qualquer coisa com Symbol.asyncIterator )Iterable (qualquer coisa com Symbol.iterator )Promise (ou qualquer então possível) Além disso, qualquer método na plataforma que deseje aceitar um argumento observável como um Web IDL, ou retornar um de um retorno de chamada cujo tipo de retorno é Observable também pode fazê -lo com qualquer um dos objetos acima, que é convertido automaticamente a um observável. Podemos conseguir isso de duas maneiras que finalizaremos na especificação observável:
Observable de um tipo de IDL da Web especial que executa este objeto Ecmascript ➡️ Conversão da Web IDL automaticamente, como o Web IDL faz para outros tipos.any e ter a prosa de especificação correspondente imediatamente invocar um algoritmo de conversão que a especificação observável fornecerá. Isso é semelhante ao que o padrão de fluxos faz com os iteráveis assíncronos hoje.A conversa no #60 se inclina para a opção (1).
Crucialmente, os observáveis são "preguiçosos", pois não começam a emitir dados até serem assinados, nem fila nenhum dado antes da assinatura. Eles também podem começar a emitir dados de forma síncrona durante a assinatura, diferentemente das promessas que sempre fazem fila microtasks ao invocar os manipuladores .then() . Considere este exemplo:
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" Usando AbortController , você pode cancelar a inscrição de um observável, mesmo que emite síncronemente dados durante a assinatura:
// 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 } ,
} ) ; É fundamental para um assinante observável poder registrar um retorno de chamada arbitrária para limpar quaisquer recursos relevantes para a assinatura. A desmontagem pode ser registrada dentro do retorno de chamada de assinatura passado para o construtor Observable . Quando executado (após a assinatura), o retorno de chamada de assinatura pode registrar uma função de desmontagem via subscriber.addTeardown() .
Se o assinante já foi abortado (isto é, subscriber.signal.aborted é true ), o retorno de chamada do desmontagem dado será chamado imediatamente de addTeardown() . Caso contrário, é invocado de maneira síncrona:
complete() , após o manipulador completo do assinante (se houver) for invocadoerror() , após o manipulador de erros do assinante (se houver) ser invocado Propomos os seguintes operadores, além da interface Observable :
catch()Promise#catch() , é preciso um retorno de chamada que é demitido após os erros observáveis da fonte. Em seguida, ele mapeará para um novo observável, devolvido pelo retorno de chamada, a menos que o erro seja renegado.takeUntil(Observable)finally()Promise.finally() , é preciso um retorno de chamada que seja disparado após a conclusão do observável de qualquer maneira ( complete() / error() ).Observable que reflete exatamente a fonte observável. O retorno de chamada passado para finally é disparado quando uma assinatura do observável resultante é encerrada por qualquer motivo . Imediatamente depois que a fonte concluir ou erros, ou quando o consumidor cancela as inscrições abortando a assinatura. As versões dos itens acima estão frequentemente presentes nas implementações de terras de usuário de observáveis, pois são úteis por razões específicas observáveis, mas, além delas, oferecemos um conjunto de operadores comuns que seguem o precedente da plataforma existente e podem aumentar bastante a utilidade e a adoção. Eles existem em outros iteáveis e são derivados da proposta de ajudantes de iterador do TC39, que adiciona os seguintes métodos ao Iterator.prototype :
map()filter()take()drop()flatMap()reduce()toArray()forEach()some()every()find() E o seguinte método estaticamente no construtor Iterator :
from() Esperamos que as bibliotecas de terras do usuário forneçam mais operadores de nicho que se integram à API Observable central a esta proposta, o envio potencialmente de maneira nativamente se eles tiverem impulso suficiente para se formar na plataforma. Mas, para esta proposta inicial, gostaríamos de restringir o conjunto de operadores àqueles que seguem o precedente declarado acima, semelhante à forma como as APIs da plataforma da Web que são declaradas como o conjunto e o MAPLIE têm propriedades nativas inspiradas no mapa do TC39 e nos objetos definidos. Portanto, consideraríamos a maior parte da discussão sobre a expansão desse conjunto como fora do escopo para a proposta inicial , adequada para discussão em um apêndice. Qualquer longa cauda de operadores pode ser concebível se houver suporte para a API observável nativa apresentada neste explicador.
Observe que os operadores every() , find() , some() e reduce() Return Promises cuja programação difere da dos observáveis, o que às vezes significa manipuladores de eventos que chamam e.preventDefault() serão tarde demais. Veja a seção de preocupações que entram em mais detalhes.
Para ilustrar como os observáveis se encaixam no cenário atual de outras primitivas reativas, consulte a tabela abaixo, que é uma tentativa de combinar outras duas tabelas que classificam primitivas reativas por sua interação com produtores e consumidores:
| Singular | Plural | |||
|---|---|---|---|---|
| Espacial | Temporal | Espacial | Temporal | |
| Empurrar | Valor | Promessa | Observável | |
| Puxar | Função | Iterador assíncrono | Iterável | Iterador assíncrono |
Os observáveis foram propostos pela primeira vez à plataforma no TC39 em maio de 2015. A proposta não conseguiu tração, em parte devido a alguma oposição de que a API era adequada para ser uma primitiva no nível do idioma. Em uma tentativa de renovar a proposta em um nível mais alto de abstração, uma questão do WhatWG DOM foi registrada em dezembro de 2017. Apesar da ampla demanda do desenvolvedor, muita discussão e nenhum ojamento forte, a proposta de observação do DOM ficou parada por vários anos (com algum fluxo no design da API) devido à falta de priorização do implementador.
Mais tarde, em 2019, uma tentativa de reviver a proposta foi feita no repositório original do TC39, que envolveu algumas simplificações da API e adicionou suporte ao problema síncrono de "casos de fogo".
Esse repositório é uma tentativa de respirar novamente a proposta observável, com a esperança de enviar uma versão para a plataforma da web.
Em discussão anterior, Ben Lesh listou várias implementações personalizadas de terras de usuários de primitivas observáveis, das quais o RXJS é o mais popular entre "mais de 47.000.000 downloads por semana ".
start e unsubscribe para observação e adquirir a Subscription antes do retorno.Actor , para permitir que as assinaturas mudem no estado, conforme mostrado em seu gancho useActor . O uso de um observável idêntico também é uma parte documentada da máquina de estado de acesso muda ao usar o XSTATE com SOLIDJS.{ subscribe(callback: (value: T) => void): () => void } padrão em seu roteador e código adiado. Isso foi apontado pelos mantenedores como inspirado pelo observável.{ subscribe(callback: (value: T) => void): () => void } interface para seus sinais.{ subscribe(callback: (value: T) => void): () => void } em vários lugares{ subscribe(callback: (value: T) => void): () => void } .{ observe_(callback: (value: T)): () => void } .| async Funcionalidade | async "tubo assíncrono" em modelos.Dada a extensa arte anterior nesta área, existe um "contrato observável" público.
Além disso, muitas APIs de JavaScript estão tentando aderir ao contrato definido pela proposta do TC39 a partir de 2015. Para esse fim, existe uma biblioteca, observável de símbolos, que os preenchimentos de pôneis (poli-preenchimentos) Symbol.observable symbol-observable possui 479 pacotes dependentes no NPM e é baixado mais de 13.000.000 vezes por semana. Isso significa que existem no mínimo 479 pacotes no NPM que estão usando o contrato observável de alguma forma.
Isso é semelhante a como as promessas/A+ especificações desenvolvidas antes da Promise foram adotadas no ES2015 como uma linguagem de primeira classe primitiva.
Uma das principais preocupações expressas no thread original Whatwg Dom tem a ver com APIs de captação de promessas no observável, como o first() proposto (). A pistola em potencial aqui com programação de microtask e integração de eventos. Especificamente, o seguinte código de aparência inocente nem sempre funcionaria:
element
. when ( 'click' )
. first ( )
. then ( ( e ) => {
e . preventDefault ( ) ;
// Do something custom...
} ) ; Se Observable#first() retornar uma promessa que resolver quando o primeiro evento for disparado em um EventTarget , então o manipulador de promessa .then() será executado:
element.click() )e.preventDefault() terá acontecido tarde demais e efetivamente foi ignoradoNo WebIDL, após o invocado de um retorno de chamada, o algoritmo HTML limpa após a execução do script é chamado, e esse algoritmo chama execute um ponto de verificação do microtask se e somente se a pilha JavaScript estiver vazia.
Concretamente, isso significa para element.click() No exemplo acima, ocorrem as seguintes etapas:
element.click() , um contexto de execução de JavaScript é primeiro empurrado para a pilhaclick interno (aquele criado nativamente pela implementação Observable#from() ), outro contexto de execução de JavaScript é empurrado para a pilha, enquanto o webidl se prepara para executar o retorno de chamada internoObservable#first() ; Agora, a fila do Microtask contém o manipulador de manipulador de compra do usuário da promessa then() que cancelará o evento assim que executarclick interno é executado, o restante do caminho do evento continua, pois o evento não foi cancelado durante ou imediatamente após o retorno de chamada. O evento faz o que faria normalmente (envie o formulário, alert() o usuário etc.)element.click() está concluído e o contexto final de execução é retirado da pilha e a fila do microtask é lavada. O manipulador .then() fornecido pelo usuário é executado, que tenta cancelar o evento tarde demais Duas coisas mitigam essa preocupação. Primeiro, há uma solução alternativa muito simples para sempre evitar o caso em que o seu e.preventDefault() pode ser tarde demais:
element
. when ( 'click' )
. map ( ( e ) => ( e . preventDefault ( ) , e ) )
. first ( ) ; ... ou se Observable tinha um método .do() (consulte Whatwg/dom#544 (Comentário)):
element
. when ( 'click' )
. do ( ( e ) => e . preventDefault ( ) )
. first ( ) ; ... ou modificando a semântica de first() para receber um retorno de chamada que produz um valor que a promessa retornada resolve:
el . when ( 'submit' )
. first ( ( e ) => e . preventDefault ( ) )
. then ( doMoreStuff ) ;Segundo, essa "peculiaridade" já existe no próspero ecossistema observável de hoje, e não há preocupações ou relatórios sérios dessa comunidade de que os desenvolvedores estão sempre se deparando com isso. Isso dá alguma confiança de que assar esse comportamento na plataforma da web não será perigoso.
Houve muita discussão sobre qual local de padrões deve sediar uma proposta observável. O local não é inconseqüente, pois decide efetivamente se os observáveis se tornam um primitivo em nível de idioma, como Promise , que são enviadas em todos os motores do navegador JavaScript, ou uma plataforma da web primitiva com a consideração provável (mas tecnicamente opcional ) em outros ambientes como o Node.js (consulte AbortController por exemplo).
Observáveis integram propositalmente o fricção com a interface principal emissor de eventos ( EventTarget ) e o cancelamento primitivo ( AbortController ) que vivem na plataforma da web. Conforme proposto aqui, os observáveis se juntam a este componente fortemente conectado existente do padrão DOM: Observables dependem do abortcontroller/abortSignal, que dependem do EventTarget, e o EventTarget depende de observáveis e do abortcontroller/abortSignal. Como sentimos que os observáveis se encaixam no melhor onde vivem seus primitivos de apoio, o local do Whatwg Standards é provavelmente o melhor lugar para avançar nessa proposta. Além disso, incorporadores de ecmascript sem web como Node.js e Deno ainda poderiam adotar observáveis e têm até mesmo o compromisso com o compromisso de abortamento e eventos da plataforma da web.
Isso não impede a padronização futura das primitivas de emissão de eventos e cancelamento no TC39 no futuro, algo que os observáveis poderiam teoricamente ser colocados em camadas mais tarde. Mas, por enquanto, estamos motivados a progredir no Whatwg.
Na tentativa de evitar a relitigação dessa discussão, pediríamos ao leitor que ver os seguintes comentários da discussão:
Esta seção descobre uma coleção de posições de padrões e padrões da Web em questões usadas para rastrear a vida da proposta observável fora deste repositório.
Os observáveis são projetados para tornar o manuseio de eventos mais ergonômico e composível. Como tal, seu impacto nos usuários finais é indireto, chegando amplamente na forma de usuários que precisam baixar menos JavaScript para implementar padrões para os quais os desenvolvedores atualmente usam bibliotecas de terceiros. Conforme declarado acima no explicador, há um próspero ecossistema de observações do Userland, que resulta em cargas de bytes excessivos sendo baixados todos os dias.
Na tentativa de codificar o forte precedente da API observável, esta proposta economizaria dezenas de implementações personalizadas de ser baixado todos os dias.
Além disso, como uma API como EventTarget , AbortController e uma relacionada a Promise , permite que os desenvolvedores construam fluxos de manuseio de eventos menos complicados, construindo-os declarativamente, o que pode permitir que eles construam mais experiências de usuário sólidas na Web.