Это объяснитель для наблюдаемого предложения API для более эргономичной и композиционной обработки событий.
EventTarget Integration Это предложение добавляет метод .when() в EventTarget , который становится лучшим addEventListener() ; В частности, он возвращает новую Observable , которая добавляет нового слушателя событий в цель при вызове его метода subscribe() . Наблюдаемый вызывает обработчик next() абонента с каждым событием.
Обработка, фильтрация и прекращение Observables Turneables в явную, декларативную поток, который легче понять и сочинять, чем сегодняшняя императивная версия, которая часто требует вложенных вызовов для 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 , так что сообщение о подписке отправляется в подключении, а сообщение Unsubscription отправляется на сервер, когда пользователь отписывает подпись.
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 ( ) ; Здесь мы используем Observables, чтобы соответствовать секретному коду, который является шаблоном клавиш, которые пользователь может нажать при использовании приложения:
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() , чтобы быть составленным и преобразованным без паутины вложенных обратных вызовов Более того, переход от обработчиков событий ➡ Вспомогательные, проще, чем у обратных вызовов ➡ обещания, поскольку Observables хорошо интегрируется на вершине EventTarget , де -факто способы подписки на события с платформы и пользовательского сценария. В результате разработчики могут использовать наблюдаемые, не мигрируя тонны кода на платформе, так как это легко везде, где бы вы ни занимались событиями.
Предлагаемую форму API можно найти в https://wicg.github.io/observable/#core-infrastructure.
Создатель наблюдаемых проходов в обратном вызове, который вызывается синхронно, когда называется subscribe() . Метод subscribe() может быть вызван любым количеством раз , и обратный вызов, который он вызывает, устанавливает новую «подписку», зарегистрировав абонента subscribe() в качестве наблюдателя. При этом наблюдаемый может сигнализировать о любом количестве событий наблюдателю с помощью next() обратного вызова, последовавшего за одним вызовом либо complete() , либо 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 тип A Special Web IDL, который автоматически выполняет этот объект Ecmascript ➡ Web IDL, как Web IDL делает для других типов.any , и иметь соответствующую прозу спецификации, немедленно вызывает алгоритм преобразования, который будет предоставлять наблюдаемая спецификация. Это похоже на то, что стандарт потоков делает с асинхронными итерами сегодня.Разговор в #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() .
Если абонент уже был прерван (то есть, subscriber.signal.aborted - это true ), то данный обратный вызов разрыва вызывает немедленно из addTeardown() . В противном случае это вызывается синхронно:
complete() , после того, как запрашивается полное обработчик подписчика (если есть)error() , после того, как обработчик ошибок подписчика (если есть). Мы предлагаем следующих операторов в дополнение к Observable интерфейсу:
catch()Promise#catch() , требуется обратный вызов, который уволен после того, как источник наблюдаемых ошибок. Затем он будет отображаться в новом наблюдаемом, возвращаемом обратным вызовом, если ошибка не повторно.takeUntil(Observable)finally()Promise.finally() , требуется обратный вызов, который уволен после того, как наблюдаемый завершается каким -либо образом ( complete() / error() ).Observable , который точно отражает источник, наблюдаемый. Обратный вызов, пройденное, finally спускается, когда подписка на полученное наблюдаемое заканчивается по любой причине . Либо сразу после того, как источник завершается или ошибки, либо когда потребитель отписывает подпись, прерывая подписку. Версии вышеперечисленного часто присутствуют в пользовательских реализациях наблюдаемых, поскольку они полезны по наблюдаемым специфическим причинам, но в дополнение к этому мы предлагаем набор общих операторов, которые следуют существующей платформе прецедента и могут значительно увеличить полезность и принятие. Они существуют на других итерациях и получены из предложения итератора TC39, которое добавляет следующие методы к Iterator.prototype :
map()filter()take()drop()flatMap()reduce()toArray()forEach()some()every()find() И следующий метод статически на конструкторе Iterator :
from() Мы ожидаем, что пользовательские библиотеки предоставят больше нишевых операторов, которые интегрируются со Observable центральным API в этом предложении, потенциально переводясь на изначально, если они получат достаточный импульс для выпуска на платформу. Но для этого первоначального предложения мы хотели бы ограничить набор операторов теми, кто следит за прецедентом, изложенным выше, аналогично тому, как API веб -платформы, которые объявляются, насыщенные и похожие на карту, обладают собственными свойствами, вдохновленными картой TC39 и установленными объектами. Поэтому мы рассмотрели большую часть обсуждения расширения этого набора в качестве индивидуального предложения для первоначального предложения, подходящего для обсуждения в приложении. Любой длинный хвост операторов мог бы следовать, если есть поддержка нативного наблюдаемого API, представленного в этом объяснении.
Обратите внимание, что операторы every() , find() , some() и reduce() возврата обещаний, чье планирование отличается от планирования наблюдаемых, что иногда означает обработчики событий, которые вызывают e.preventDefault() будет работать слишком поздно. Смотрите раздел «Проблемы», который входит в более подробную информацию.
Чтобы проиллюстрировать, как наблюдаемые вписываются в текущий ландшафт других реактивных примитивов, см. В приведенном ниже таблице, которая является попыткой объединить две другие таблицы, которые классифицируют реактивные примитивы путем их взаимодействия с производителями и потребителями:
| Единственный | Множественное число | |||
|---|---|---|---|---|
| Пространственный | Временный | Пространственный | Временный | |
| Толкать | Ценить | Обещать | Наблюдаемый | |
| Тянуть | Функция | Асинхровый итератор | Итерабильный | Асинхровый итератор |
Наблюдаемые были впервые предложены платформе в TC39 в мае 2015 года. Предложение не смогло получить тягу, отчасти из-за некоторой оппозиции, что API подходит для того, чтобы стать примитивом на уровне языка. В попытке продлить предложение на более высоком уровне абстракции, вопрос о том, что в декабре 2017 года была подана. Несмотря на широкий спрос разработчиков, много обсуждений и никаких сильных возражающих, предложение о наблюдении за DOM в основном все еще в течение нескольких лет (с некоторым потоком в дизайне API) из -за отсутствия приоритетов -исполнителя.
Позже, в 2019 году, попытка возродить предложение была сделана в оригинальном репозитории TC39, который включал некоторые упрощения API и добавила поддержку синхронной проблемы «пожарных».
Этот репозиторий является попыткой снова вдохнуть жизнь в наблюдаемое предложение с надеждой доставки версии на веб -платформу.
В предыдущем обсуждении Бен Леш перечислил несколько пользовательских реализаций наблюдаемых примитивов, из которых RXJS является наиболее популярным в «47 000 000+ загрузках в неделю ».
start и unsubscribe для наблюдения и получения Subscription до возврата.Actor , чтобы позволить подписки на изменения состояния, как показано в их крючке useActor . Использование идентичного наблюдаемого также является документированной частью изменений машины состояния доступа при использовании 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 трубы в шаблонах.Учитывая обширное предыдущее искусство в этой области, существует публичный «наблюдаемый контракт».
Кроме того, многие API JavaScript пытались придерживаться контракта, определяемого предложением TC39 от 2015 года. С этой целью существует библиотека, подлежащая символу, что PonyFills (Polyfills) Symbol.observable , подлежащий обращению, чтобы помочь с совместимостью между наблюдаемыми типами, которые придерживаются именно к интерфейсу, определяемому здесь. symbol-observable имеет 479 зависимых пакетов на NPM и загружается более 13 000 000 раз в неделю. Это означает, что на NPM есть минимум 479 пакетов, которые каким -то образом используют наблюдаемый контракт.
Это похоже на то, как обещания/A+ были разработаны до того, как Promise были приняты в ES2015 в качестве первоклассного языка примитива.
Одна из основных проблем, выраженных в оригинальном потоке DOM WhatWG, связана с API-интерфейсом, которые можно найти на наблюдаемых, например, предложенный first() . Потенциальная сжимная леса здесь с планированием микрозама и интеграцией событий. В частности, следующий невинно выглядящий код не всегда будет работать:
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 сначала выдвигается в стекObservable#from() запустить внутренний click вызов прослушивателя событий ClicObservable#first() ; Теперь очередь микротаски содержит обработчик then() , который отменяет событие, содержит отменуclick выполняется, остальная часть пути события продолжается, поскольку событие не было отменено в течение или сразу после обратного вызова. Событие делает все, что обычно делает (отправьте форму, alert() пользователь и т. Д.)element.click() , закончен, и окончательный контекст выполнения выскочивается из стека, а очередь микротаски промывается. Обработчик с положением пользователя .then() запускается, что пытается отменить событие слишком поздно Две вещи смягчают эту проблему. Во -первых, есть очень простой обходной путь, чтобы всегда избегать случая, когда ваш e.preventDefault() может работать слишком поздно:
element
. when ( 'click' )
. map ( ( e ) => ( e . preventDefault ( ) , e ) )
. first ( ) ; ... или если наблюдаемый был метод .do() (см. Whatwg/Dom#544 (Comment)):
element
. when ( 'click' )
. do ( ( e ) => e . preventDefault ( ) )
. first ( ) ; ... или путем изменения семантики first() чтобы получить обратный вызов, который дает значение, которое возвращаемое обещание разрешает:
el . when ( 'submit' )
. first ( ( e ) => e . preventDefault ( ) )
. then ( doMoreStuff ) ;Во -вторых, этот «причудливый» уже существует в сегодняшней процветающей наблюдаемой экосистеме, и нет серьезных проблем или отчетов от этого сообщества, что разработчики постоянно сталкиваются с этим. Это дает некоторую уверенность в том, что выпечка этого поведения в веб -платформу не будет опасной.
Было много дискуссий о том, какие стандарты должны в конечном итоге принять предложение наблюдаемого. Место проведения не является несущественным, так как оно эффективно решает, становится ли наблюдаемые примитивами на уровне языка, такими как Promise , которые поставляются во всех двигателях браузера JavaScript или примитивом веб-платформы с вероятным (но технически необязательным ) соображением в других средах, таких как Node.js (см. Например, AbortController ).
Наблюдаемые целенаправленно интегрируются без трения с основным интерфейсом, излучающим события ( EventTarget ) и примитивом отмены ( AbortController ), который живет на веб-платформе. Как предложено здесь, наблюдаемые присоединяются к этому существующему сильно подключенному компоненту из стандарта DOM: наблюдаемые зависят от AbortController/AbortSignal, которые зависят от EventTarget, а EventTarget зависит как от наблюдаемых, так и от AbortController/AbortSignal. Поскольку мы считаем, что наблюдаемые подходит в лучшем случае, где живут его вспомогательные примитивы, место стандарта WhatWG, вероятно, является лучшим местом для продвижения этого предложения. Кроме того, не Web Ecmascript Embedders, такие как Node.js и Deno, все равно смогут принять наблюдаемые и даже вероятно, учитывая их приверженность прерыванию веб-платформы и событиям.
Это не исключает будущей стандартизации примитивов и отмены событий и отмены примитивах в TC39 в будущем, что-то, что наблюдаемые теоретически могут быть наложены на последующую точку зрения. Но сейчас мы мотивированы, чтобы добиться прогресса в WhatWG.
В попытке избежать рецидива этого обсуждения мы призываем читателя увидеть следующие комментарии к обсуждению:
В этом разделе есть набор веб -стандартов и стандартов, используемых для отслеживания жизни наблюдаемого предложения за пределами этого репозитория.
Наблюдаемые предназначены для того, чтобы сделать обработку событий более эргономичным и композиционным. Таким образом, их влияние на конечных пользователей является косвенным, в значительной степени поступает в форме пользователей, необходимого загружать меньше JavaScript для реализации шаблонов, для которых разработчики в настоящее время используют сторонние библиотеки. Как указано выше в объяснении, существует процветающая экосистема пользователя, которая приводит к загрузке чрезмерных байтов.
В попытке кодифицировать сильный пользовательский прецедент наблюдаемого API, это предложение сэкономит десятки пользовательских реализаций от загрузки каждый день.
Кроме того, как API, такой как EventTarget , AbortController и один, связанный с Promise S, он позволяет разработчикам создавать менее сложные потоки обработки событий, построив их, что может позволить им создавать больше звуковых пользователей в Интернете.