這是可觀察到的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,它使開發人員能夠通過聲明性地構建它們來構建較不復雜的事件處理流,這可能使他們能夠在網絡上構建更多合理的用戶體驗。