これは、より人間工学に基づいて構成可能なイベント処理のための観察可能なAPI提案の説明者です。
EventTarget統合この提案は、より良いaddEventListener()になるEventTargetに.when()メソッドを追加します。具体的には、 subscribe()メソッドが呼び出されたときに新しいイベントリスナーをターゲットに追加する新しいObservableを返します。観察可能なものは、各イベントでサブスクライバーの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の上にうまく統合されるため、コールバックの移行よりも簡単です。その結果、開発者は、プラットフォーム上で大量のコードを移行することなく、観測可能性を使用できます。これは、今日のイベントを処理しても簡単なドロップインです。
提案されているAPI形状は、https://wicg.github.io/observable/#core-infrastructureにあります。
Observableの作成者はsubscribe()が呼び出されるたびに同期して呼び出されるコールバックでパスをパスします。 subscribe()メソッドは任意の回数と呼ばれることがあり、それが呼び出すコールバックは、 subscribe()の発信者をオブザーバーとして登録することにより、新しい「サブスクリプション」を設定します。これを導入すると、観測可能はnext()コールバックを介してオブザーバーに任意の数のイベントを信号します。オプションでは、 complete()またはerror()のいずれかへの1回の呼び出しが続き、データのストリームが終了していることを示します。
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()を呼び出すと、本質的に、オブザーバーハンドラー機能を介してイベントが公開され、すべての観測可能性が利用できるさまざまなコンビネーターと合成可能な新しいイベントリスナーを登録します。
オブザーバブルは、上記のように、ネイティブコンストラクターによって、またはstaticメソッドのObservable.from()によって作成できます。この方法は、次の順序で、次のいずれかのオブジェクトから観察可能なネイティブを構築します。
Observable (その場合、指定されたオブジェクトを返すだけです)AsyncIterable ( Symbol.asyncIteratorを備えたあらゆるもの)Iterable ( Symbol.iteratorのあるもの)Promise (またはその後のあらゆるもの)さらに、Web IDL引数として観察可能なものを受け入れたい、またはreturn型が上記のオブジェクトのいずれかを使用して自動的に変換されるコールバックからの任意の方法を希望Observable方法で、任意のメソッドが観察可能に変換されます。これは、観察可能な仕様で最終決定する2つの方法のいずれかで達成できます。
Observableなタイプを、他のタイプに対してWeb IDLが行うように、Web IDL変換を自動的に実行することにより。any指定するためにオブザーバブルズで動作するメソッドとコールバックが必要であり、対応する仕様の散文に、観察可能な仕様が提供する変換アルゴリズムをすぐに呼び出すことが必要です。これは、Streams Standardが今日のAsync Iterablesで行うことと似ています。#60の会話は、オプション(1)に傾いています。
重要なことに、観測可能性は、サブスクライブされるまでデータを発し始めたり、サブスクリプション前にデータをキューにしたりしないという点で「怠zy」です。また.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()を介して分解機能を登録できます。
サブスクライバーがすでに中止されている場合(すなわち、 subscriber.signal.abortedがtrue )、与えられた分解コールバックはaddTeardown()内からすぐに呼び出されます。それ以外の場合は、同期的に呼び出されます。
complete()から、サブスクライバーの完全なハンドラー(ある場合)が呼び出された後error()から、サブスクライバーのエラーハンドラー(ある場合)が呼び出された後Observableインターフェイスに加えて、次の演算子を提案します。
catch()Promise#catch()のように、ソースの観測可能なエラーの後に起動されるコールバックが必要です。次に、エラーが再巻き戻されない限り、コールバックによって返される新しい観測可能なものにマッピングされます。takeUntil(Observable)finally()Promise.finally()のように、観察可能な完了後に起動されるコールバックが必要です( complete() / error() )。Observableを返します。結果として得られた観測可能のサブスクリプションが何らかの理由で終了すると、 finallyに渡されたコールバックが最終的に発生されます。ソースが完了またはエラーが完了する直後、またはサブスクリプションを中止して消費者が登録解除した場合。上記のバージョンは、観測可能な理由で有用なため、観測可能性のユーザーランド実装にしばしば存在しますが、これらに加えて、既存のプラットフォームの先例に従い、有用性と採用を大幅に増やすことができる共通オペレーターのセットを提供します。これらは他の反復可能性に存在し、次の方法をIterator.prototypeに追加するTC39のIteratorヘルパーの提案から派生しています。
map()filter()take()drop()flatMap()reduce()toArray()forEach()some()every()find()そして、 Iteratorコンストラクターの次のメソッドの静的な方法:
from()ユーザーランドライブラリは、この提案の中心的にObservable APIと統合するニッチなオペレーターを提供し、プラットフォームに卒業するのに十分な勢いを得ると、ネイティブに潜在的に出荷される可能性があります。しかし、この最初の提案では、オペレーターのセットを、セットのように宣言されたWebプラットフォームAPIがTC39のマップとセットオブジェクトに触発されたネイティブプロパティをどのように宣言しているかと同様に、上記の先例に従うオペレーターに制限したいと思います。したがって、このセットを最初の提案の範囲外として拡大することについてのほとんどの議論を検討し、付録での議論に適しています。オペレーターの長い尾は、この説明担当者に提示されたネイティブの観測可能なAPIのサポートがある場合、と考えられる可能性があります。
スケジューリングが観測可能性とは異なるevery() 、 find() 、 some() 、resid() reduce()は、 e.preventDefault()を呼び出すイベントハンドラーが遅すぎることを意味する場合があることに注意してください。より詳細になる懸念セクションを参照してください。
他の反応性プリミティブの現在の景観に観察可能なものがどのように適合するかを説明するには、生産者や消費者との相互作用によってリアクティブなプリミティブを分類する他の2つのテーブルを組み合わせる試みである以下のテーブルを参照してください。
| 特異 | 複数 | |||
|---|---|---|---|---|
| 空間 | 時間的 | 空間 | 時間的 | |
| 押す | 価値 | 約束 | 観察可能 | |
| 引く | 関数 | Async Iterator | 反復可能 | Async Iterator |
Observablesは、2015年5月にTC39のプラットフォームに最初に提案されました。この提案は、APIが言語レベルの原始であることに適しているという反対のために、牽引力を得ることができませんでした。より高いレベルの抽象化で提案を更新するために、2017年12月にWhatWG DOMの問題が提出されました。十分な開発者の需要、多くの議論、強力な反対者はいませんでしたが、DOM Observablesの提案は、実装者の優先順位付けがないため、ほとんど数年間(API設計の流動性があります)。
2019年後半に、提案を復活させようとする試みが元のTC39リポジトリで返されました。これには、同期の「Firehose」問題に対するAPIの単純化と追加のサポートが含まれていました。
このリポジトリは、そのバージョンをWebプラットフォームに出荷することを期待して、再び観察可能な提案に命を吹き込む試みです。
以前の議論では、Ben Leshは、観察可能なプリミティブのいくつかのカスタムユーザーランドの実装をリストしました。そのうちのRXJは、「週あたり47,000,000以上のダウンロード」で最も人気があります。
startイベントとunsubscribeのための登録解除と返品前にSubscriptionを取得するためのほぼ同一の契約。useActorフックに示されているように、ライブラリのいくつかの場所、特にActorタイプのいくつかの場所で観測可能なインターフェイスを使用して、状態の変更を加算できるようにします。 XSTATEを使用してXSTATEを使用する場合、同じ観測可能なものを使用することは、アクセス状態マシンの変更の文書化された部分でもあります。{ subscribe(callback: (value: T) => void): () => void }パターンを使用します。これは、メンテナーによって、Observableに触発されていると指摘されていました。{ 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 「Async Pipe」機能。この分野での広範な以前のアートを考えると、公的な「観察可能な契約」が存在します。
さらに、多くのJavaScript APIは、2015年からTC39の提案によって定義された契約を遵守しようとしています。そのためには、ここで定義されたインターフェースに付着する観察可能なタイプ間の相互運用性を助けるために、ポニーフィル(ポリフィル) Symbol.observable 。 symbol-observableには、npmに479の従属パッケージがあり、週に13,000,000回以上ダウンロードされています。これは、何らかの方法で観測可能な契約を使用しているNPMには最低479個のパッケージがあることを意味します。
これは、 Promise sが第一クラスの言語原始としてES2015に採用される前に開発されたPROMISES/A+仕様に似ています。
元のWhatWG DOMスレッドで表明された主な懸念の1つは、提案されたfirst()など、観察可能な約束のAPIに関係しています。ここでの潜在的なフットガンは、マイクロタスクのスケジューリングとイベント統合です。具体的には、次の無邪気なコードが常に機能するとは限りません。
element
. when ( 'click' )
. first ( )
. then ( ( e ) => {
e . preventDefault ( ) ;
// Do something custom...
} ) ; Observable#first()がEventTargetで最初のイベントが起動されたときに解決する約束を返した場合、ユーザーがサプリされた約束.then()ハンドラーが実行されます。
element.click() )e.preventDefault()遅すぎて効果的に無視されることを意味しますWebIDLでは、コールバックが呼び出された後、実行されたスクリプトが呼び出された後にHTMLアルゴリズムがクリーンアップし、このアルゴリズム呼び出しがJavaScriptスタックが空である場合にのみマイクロタスクチェックポイントを実行します。
具体的には、それはelement.click()を意味します。上記の例では、次の手順が発生します。
element.click()を実行するには、JavaScriptの実行コンテキストが最初にスタックに押し込まれますclickイベントリスナーコールバックを実行するために( Observable#from()によってネイティブに作成されたもの)、WebIDLが内部コールバックを実行する準備をするため、別のJavaScript実行コンテキストがスタックにプッシュされますObservable#first()によって返される約束がすぐに解決されます。これで、MicrotaskキューにはPromiseのユーザーがサプリしたthen()ハンドラーが含まれています。clickイベントコールバックが実行された後、イベントがコールバック中または直後にキャンセルされなかったため、残りのイベントパスは続きます。イベントは通常何をするかを行います(フォーム、 alert()ユーザーなどを送信します)element.click()を含むJavaScriptが終了し、最終的な実行コンテキストがスタックからポップされ、マイクロタスクキューがフラッシュされます。ユーザーサプリド.then()ハンドラーが実行されます。これはイベントをキャンセルしようとしますこの懸念を軽減する2つのこと。まず、 e.preventDefault()が遅すぎる可能性がある場合を常に回避するための非常に簡単な回避策があります。
element
. when ( 'click' )
. map ( ( e ) => ( e . preventDefault ( ) , e ) )
. first ( ) ; ...または、Observableが.do()メソッドを持っていた場合(Whatwg/dom#544(コメント)を参照):
element
. when ( 'click' )
. do ( ( e ) => e . preventDefault ( ) )
. first ( ) ; ...または、 first()のセマンティクスを変更して、返された約束が解決する値を生成するコールバックを取得することにより、
el . when ( 'submit' )
. first ( ( e ) => e . preventDefault ( ) )
. then ( doMoreStuff ) ;第二に、この「Quirk」はすでに繁栄している観察可能なエコシステムに存在しており、そのコミュニティからの深刻な懸念や報告は、開発者がこれに一貫して走っているという報告はありません。これにより、この動作をWebプラットフォームに焼くことは危険ではないという自信が与えられます。
どの基準会場が最終的に観察可能な提案を開催すべきかについて多くの議論がありました。会場は、観測可能性がPromiseのような言語レベルの原始的になるかどうか、すべてのJavaScriptブラウザエンジンに出荷されるか、Node.jsなどの他の環境で可能性のある(技術的にオプションの)考慮事項を持つ原始的なWebプラットフォームであるかどうかを効果的に決定するため、取るに足らないものではありません(たとえば、 AbortController参照)。
Observablesは、Webプラットフォームに住んでいるメインイベントエミッティングインターフェイス( EventTarget )およびキャンセルプリミティブ( AbortController )と意図的に摩擦を統合します。ここで提案されているように、観測可能性は、DOM標準からこの既存の強く接続されたコンポーネントに加わります。観測品は、イベントタルゲットに依存するAbortController/Abortsignalに依存し、EventTargetは観測可能性とAbortController/Abortsignalの両方に依存します。オブザーバブルは、そのサポートプリミティブが住んでいる場所で最も適合していると感じているため、WhatWG Standards Venueはおそらくこの提案を進めるのに最適な場所です。さらに、Node.jsやDenoなどの非Web ECMAScriptの埋め込み剤は、Webプラットフォームの中止やイベントへのコミットメントを考慮して、観察可能なものを採用することができ、さらにはそうなる可能性もあります。
これは、将来TC39のイベントおよびキャンセルプリミティブの将来の標準化を妨げるものではなく、観測可能性は理論的には後で階層化される可能性があります。しかし今のところ、私たちはWhatwgを進歩させる動機です。
この議論の関係を避けようとするために、読者に次の議論のコメントを見るように促します。
このセクションでは、このリポジトリの外で観察可能な提案の生活を追跡するために使用されるWeb標準と標準の位置の問題のコレクションを紹介します。
オブザーバブルは、イベントの処理をより人間工学に基づいて構成可能にするように設計されています。そのため、エンドユーザーへの影響は間接的であり、開発者が現在サードパーティライブラリを使用しているパターンを実装するために、より少ないJavaScriptをダウンロードしなければならないという形で主に登場します。説明者に上記のように、繁栄しているユーザーランドオブザーバブルエコシステムがあり、毎日過剰なバイトがダウンロードされています。
観察可能なAPIの強力なユーザーランドの先例を成文化するために、この提案は、毎日数十のカスタム実装がダウンロードされるのを防ぐことができます。
さらに、 EventTarget 、 AbortController 、およびPromise Sに関連するAPIとして、開発者は宣言的にそれらを構築することにより、より複雑なイベント処理フローを構築することができます。