这是可观察到的API提案的解释者,以进行更符合人体工程学和可复合事件处理。
EventTarget集成该提案将.when()方法添加到EventTarget中,该方法变成了更好的addEventListener() ;具体而言,它返回了一个新的Observable的可添加新事件侦听器的新事件侦听器,当它subscribe()方法被调用。可观察到的每个事件都会调用订户的next()处理程序。
可观察到的事件处理,过滤和终止变成明确的声明流,比今天的命令式版本更容易理解和构成,后者通常需要嵌套的调用来addEventListener()和难以遵循的回调链。
// 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 } ) ; 跟踪容器中的所有链接点击(示例):
container
. when ( 'click' )
. filter ( ( e ) => e . target . closest ( 'a' ) )
. subscribe ( {
next : ( e ) => {
// …
} ,
} ) ; 在保持鼠标时找到最大y坐标(示例):
const maxY = await element
. when ( 'mousemove' )
. takeUntil ( element . when ( 'mouseup' ) )
. map ( ( e ) => e . clientY )
. reduce ( ( soFar , y ) => Math . max ( soFar , y ) , 0 ) ; 多路复用WebSocket ,以便在连接上发送订阅消息,并且当用户取消订阅时,将发送未取消标准消息。
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 ( ) ; 在这里,我们利用可观察到的秘密代码匹配,这是用户使用应用程序时可能会击中的键的模式:
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 API观察物是代表可重复的事件的一流对象。它们就像承诺一样,但对于多个事件,特别是在EventTarget集成中,它们是对回调的承诺。他们可以:
subscribe()消费事件感兴趣的任何人Observable.map()之类的运营商,在没有嵌套回调的情况下进行组成和转换更好的是,从事件处理程序➡️观察者的过渡比回调更简单➡️承诺,因为观察值在EventTarget上很好地集成了EventTarget,这是从平台和自定义脚本订阅事件的事实上的方式。结果,开发人员可以在平台上使用可观察到的物品,而无需在平台上迁移大量的代码,因为无论您当今在哪里处理事件,都可以轻松放入。
提出的API形状可以在https://wicg.github.io/observable/#core-infrastructure中找到。
可观察到的通行证的创建者在回调中,该回调在调用subscribe()时会同步调用。可以将subscribe()方法称为多次,通过将subscribe()的呼叫者注册为观察者,它调用的回调设置了新的“订阅”。有了此处,可观察的可通过next()回调向观察者发出任何数量的事件,然后选择单个呼叫对complete()或error()或error() ,表明数据流已经完成。
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 ,
} ) ;虽然自定义可观察物可以自己有用,但他们解锁的主要用例是事件处理。由新EventTarget#when()方法返回的可观察物是使用与addEventListener()相同的基础机制的内部回调创建的。因此,调用subscribe()本质上是注册一个新事件侦听器,其事件通过观察者处理程序功能暴露,并且与所有可观察到的各种组合者都可以合并。
如上所述,可以通过其本机构造函数或Observable.from()静态方法创建可观察到的物品。此方法按以下任何顺序构建可观察到的本机可观察到的对象:
Observable (在这种情况下,它只是返回给定的对象)AsyncIterable (任何具有Symbol.asyncIterator东西)Iterable ( Symbol.iterator的任何东西)Promise (或任何当时的)此外,平台上希望接受可观察到的Web IDL参数的任何方法,或从Observable返回类型的回调中返回一个方法,也可以使用以上任何对象来执行此操作,从而自动转换为可观察到的。我们可以通过可观察到的规范来完成的两种方法之一来实现这一目标:
Observable类型成为特殊的Web IDL类型,该类型可以自动执行此Ecmascript对象➡️Web IDL转换,就像Web IDL对其他类型一样。any类型,并具有相应的规格散文,立即调用可观察规范将提供的转换算法。这类似于Streams标准在当今的异步迭代率的作用。#60中的对话倾向于选项(1)。
至关重要的是,可观察到的是“懒惰”,因为它们在订阅订阅之前不会开始发射数据,也不会在订阅前排队任何数据。他们也可以在订阅期间同步发射数据,这与诺言时总是在调用.then()处理程序时始终排队的微型掩体。考虑此示例:
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" 通过使用AbortController ,即使在订阅过程中同步排放数据:
// 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 } ,
} ) ; 对于可观察到的订户来说,能够注册任意拆除回调以清理与订阅相关的任何资源至关重要。拆卸可以从传递到Observable构造函数的订阅回调中注册。运行(订阅时)时,订阅回调可以通过subscriber.addTeardown()注册拆卸功能。
如果订户已经中止( true subscriber.signal.aborted ),则立即从addTeardown()中立即调用给定的拆卸回调。否则,将其同步调用:
complete() ,订户的完整处理程序(如果有)被调用后error() ,订户的错误处理程序(如果有)被调用之后除了Observable接口外,我们还建议以下操作员:
catch()Promise#catch()一样,它需要一个回调,该回调在源可观察到的错误之后被触发。然后,它将映射到可通过回调返回的新的可观察到的新观察,除非重新返回错误。takeUntil(Observable)finally()Promise.finally()一样,它需要一个回调,该回调在可观察到的任何方式完成后被触发(pountty( complete() / error() )。Observable可观察到可观察到的源的可观察。当由于任何原因终止对结果可观察到的可观察到的订阅时,回电finally被解雇。来源完成或错误之后,或者是通过中止订阅而取消订阅的情况。上述版本通常存在于可观察到的用户领域实现中,因为它们是出于可观察的特定原因有用的,但是除此之外,我们还提供了一组遵循现有平台先例的常见运营商,并且可以大大提高实用性和采用。这些存在于其他迭代物上,并且源自TC39的迭代器帮助者的建议,该建议将以下方法添加到Iterator.prototype :Prototype:
map()filter()take()drop()flatMap()reduce()toArray()forEach()some()every()find()以及以下在Iterator构造函数上静态的方法:
from()我们希望Userland图书馆提供更多的利基运营商,与该提案的Observable API集成,如果他们获得足够的动力来毕业于平台,则可能会在本地运输。但是,对于此最初的建议,我们想将一组运算符限制在遵循上面所述的先例的操作员中,类似于声明为setlike和mapike的Web平台API具有受TC39映射和设置对象启发的本机属性。因此,我们将大多数将该集合扩展的讨论视为最初提案的范围内,适用于附录中的讨论。如果对本解释器中提出的可观察到的原始可观察的API有支持,则可以想象的任何长尾巴都可以追随。
请注意,操作员every() , find() , some()和reduce()返回承诺的计划与可观察到的计划不同,这有时意味着致电e.preventDefault()事件处理程序会太晚运行。请参阅更详细的关注部分。
为了说明观察力如何适合其他反应性基原始人的当前景观,请参见下表,该表是尝试通过与生产者和消费者的互动来结合两个将反应性原始词分类的表格:
| 单数 | 复数 | |||
|---|---|---|---|---|
| 空间 | 颞 | 空间 | 颞 | |
| 推 | 价值 | 承诺 | 可观察 | |
| 拉 | 功能 | 异步迭代器 | 觉得 | 异步迭代器 |
最初是在2015年5月在TC39的平台上提出的。该提案未能获得吸引力,部分原因是API适合作为语言级别的原始性。为了在较高的抽象水平上续签该提案,2017年12月提出了WATWG DOM问题。尽管有充足的开发人员需求,大量讨论和没有强有力的反对者,但由于缺乏实施者优先级,该dom observables提案大多持续了几年(由于API设计而有些磁带)。
2019年晚些时候,在原始的TC39存储库中进行了恢复提案的尝试,该库涉及一些API简化,并增加了对同步“ Firehose”问题的支持。
该存储库是试图再次将生命重新注入可观察的建议,希望将其版本运送到Web平台上。
在先前的讨论中,本·莱什(Ben Lesh)列出了几种可观察基础的自定义Userland实现,其中RXJS最受欢迎,“每周下载47,000,000多种”。
start和unsubscribe事件,以观察并在退货之前获取Subscription 。Actor类型)中使用可观察到的接口,以允许订阅状态的变化,如其useActor Hook所示。使用相同的可观察到的可观察的部分也是访问状态计算机使用XSTATE与SOLIDJS的记录一部分。{ subscribe(callback: (value: T) => void): () => void }模式在其路由器和deferreddata代码中。维护者指出,这是受可观察的启发。{ subscribe(callback: (value: T) => void): () => void }的信号接口。{ subscribe(callback: (value: T) => void): () => void }在多个位置}的订阅接口{ subscribe(callback: (value: T) => void): () => void } 。{ observe_(callback: (value: T)): () => void } 。| async隐式订阅和未订阅的可观察结果。模板中的| async “异步管”功能。鉴于该领域有广泛的现有艺术,存在公众的“可观察合同”。
此外,许多JavaScript API一直在试图遵守2015年TC39提案所定义的合同。到此为止,有一个库,可观察到的符号,该图书馆(Polyfills) Symbol.observable可在可观察的类型之间互操作性,以帮助可观察到的类型可互操作,从而确定此处定义的界面定义了。 symbol-observable NPM上有479个依赖包,每周下载超过13,000,000次。这意味着NPM上至少有479个包裹以某种方式使用可观察的合同。
这类似于在Promise s作为ES2015作为一流语言原始语言中采用之前开发的承诺/A+规范。
原始Whatwg dom线程中表达的主要问题之一与在可观察到的可观察到的API(例如拟议的first()有关。在这里具有微型调度和事件集成的潜在mogun。具体而言,以下无辜的代码并不总是有效:
element
. when ( 'click' )
. first ( )
. then ( ( e ) => {
e . preventDefault ( ) ;
// Do something custom...
} ) ;如果Observable#first()返回在EventTarget上触发第一个事件时解决的承诺,则用户支持的承诺.then()
element.click() )e.preventDefault()将发生太晚并有效地被忽略在调用回调后,在webidl中,调用了运行脚本后的HTML算法清理,并且此算法调用执行MicroTask检查点,并且仅当JavaScript堆栈为空时。
具体而言,这意味着element.click()在上面的示例中,发生以下步骤:
element.click() ,首先将JavaScript执行上下文推入堆栈click事件侦听器回调( Observable#from()本地创建的回调),将另一个JavaScript执行上下文推向堆栈,因为WebIdl准备运行内部回调Observable#first()返回的承诺;现在,Microtask队列包含Promise的用户供给, then()处理程序,该处理程序将在活动运行后取消该事件click事件回调后,由于事件在回调期间或立即取消,事件路径的其余部分仍在继续。该事件可以做任何通常会做的事情(提交表单, alert()用户等)element.click()已完成,最终的执行上下文从堆栈中弹出,微型掩盖队列被冲洗。用户供给.then()处理程序,试图取消事件太晚两件事减轻了这种关注。首先,有一个非常简单的解决方法可以始终避免您的e.preventDefault()运行得太晚的情况:
element
. when ( 'click' )
. map ( ( e ) => ( e . preventDefault ( ) , e ) )
. first ( ) ; ...或者如果可观察到有.do()方法(请参阅Whatwg/dom#544(注释)):
element
. when ( 'click' )
. do ( ( e ) => e . preventDefault ( ) )
. first ( ) ; ...或通过修改first()的语义来进行回调,该回调产生一个返回的承诺可以解决的价值:
el . when ( 'submit' )
. first ( ( e ) => e . preventDefault ( ) )
. then ( doMoreStuff ) ;其次,这个“怪癖”已经存在于当今蓬勃发展的可观察到的生态系统中,该社区没有严重的担忧或报告,即开发人员一直在努力。这使人们有信心将这种行为烘烤到Web平台上不会是危险的。
关于哪个标准场地应最终应该主持可观察的建议,进行了很多讨论。该场地并不是无关紧要的,因为它有效地决定了可观察到的词级原始词,如Promise S,该船在所有JavaScript浏览器发动机中,还是在Node.js等其他环境中具有可能(但技术上可选)考虑的Web平台(例如,请参见AbortController )。
可观察到现场的主要事件发射接口( EventTarget )和取消原始( AbortController )无用的摩擦无用。如这里提出的那样,可观察到的可观察到来自DOM标准的现有强烈连接的组件:可观察结果取决于依赖eventtarget的abortcontroller/Aprotsignal,并且EventTarget取决于观察值和AbortController/Aportsignal。因为我们认为可观察到的是最适合其支持原语的地方的最佳选择,所以Whatwg标准场地可能是推进这一建议的最佳场所。此外,鉴于他们对Web平台流产和事件的承诺,node.js和deno等非WEB ecmascript嵌入者仍然能够采用可观察到的东西。
这并不排除未来TC39中事件发射和取消原始的标准化,从理论上讲,观察物可以在后来的顶部进行分层。但是就目前而言,我们有动力在Whatwg中取得进步。
为了避免进行讨论,我们敦促读者查看以下讨论评论:
本节列出了网络标准和标准位置的集合,用于跟踪该存储库以外的可观察建议的生活。
可观察的旨在使事件处理更加符合人体工程学和合并。因此,它们对最终用户的影响是间接的,主要是用户以较少下载JavaScript的形式出现的,以实现开发人员当前使用第三方库的模式。如上所述,有一个蓬勃发展的Userland可观察到生态系统,每天都会下载过多的字节。
为了将可观察到的API的强大用户领域进行编纂,该提案将节省数十个自定义实现,以免每天下载。
此外,作为EventTarget , AbortController和Promise s这样的API,它使开发人员能够通过声明性地构建它们来构建较不复杂的事件处理流,这可能使他们能够在网络上构建更多合理的用户体验。