目前,在JavaScript中处理一系列异步结果的唯一稳定方法是使用事件系统。然而,目前正在开发三种替代方案:
流:https://streams.spec.whatwg.org
可观察:https://tc39.github.io/proposal-observable
异步迭代程序:https://tc39.github.io/proposal-async-iteration
与其他事件相比,每种事件有什么区别和好处?
其中任何一个打算取代事件吗?
这里大约有两类API:pull和push。
拉动
异步拉取API非常适合从源中拉取数据的情况。这个源可能是一个文件、一个网络套接字、一个目录列表或其他任何东西。关键是,当被要求时,要从源中提取或生成数据。
异步迭代器是这里的基本原语,旨在作为基于拉的异步源概念的通用表现。在这样一个来源,你:
- 通过执行
const promise = ai.next()
从异步迭代器中提取 - 使用
const result = await promise
(或使用.then()
)等待结果 - 检查结果以确定它是异常(抛出)、中间值(
{ value, done: false })
)还是完成信号({ value: undefined, done: true }
)
这类似于同步迭代器是基于拉的同步值源概念的一般表现。同步迭代器的步骤与上面完全相同,省略了"等待结果"步骤。
可读流是异步迭代器的一种特殊情况,旨在专门封装套接字/文件等I/O源。它们有专门的API,用于将它们输送到可写流(代表I/O生态系统的另一半,汇)并处理由此产生的背压。它们还可以专门用于以高效的"自带缓冲区"方式处理字节。这一切都让人想起了数组是同步迭代器的特殊情况,它针对O(1)索引访问进行了优化。
pull API的另一个特性是它们通常是单一消费者。无论是谁提取了值,现在都有了它,而且它不存在于源异步迭代器/stream/etc中。不再它被消费者拿走了。
一般来说,pull API提供了一个与一些底层数据源通信的接口,允许消费者表达对它的兴趣
Push
推送API非常适合生成数据,并且生成的数据不在乎是否有人想要。例如,不管是否有人感兴趣,你的鼠标移动了,然后你点击了某个地方,这仍然是真的。您可能希望通过推送API来显示这些事实。然后,消费者——可能是他们中的多个——可能会订阅,并被推送有关此类事件发生的通知。
API本身并不关心是零、一还是许多消费者订阅。它只是展示了一个关于宇宙中发生的事情的事实。
事件就是这方面的一个简单表现。您可以在浏览器中订阅EventTarget,或在Node.js中订阅EventEmitter,并收到调度事件的通知。(通常但不总是由EventTarget的创建者执行。)
Observables是EventTarget的一个更精细的版本。他们的主要创新是订阅本身由一个一流的对象Observable表示,然后您可以对其应用组合子(如filter、map等)。他们还选择将三个信号(通常称为next、complete和error)捆绑在一起,并赋予这些信号特殊的语义,以便组合子尊重它们。这与EventTarget相反,在EventTarget中,事件名称没有特殊的语义(EventTarget的任何方法都不关心事件的名称是"complete"还是"asdf")。Node中的EventEmitter有这种特殊语义方法的某些版本,其中"错误"事件可能会使进程崩溃,但这相当原始。
可观察事件的另一个好特性是,通常只有可观察的创建者才能使其生成下一个/错误/完整的信号。而在EventTarget上,任何人都可以调用dispatchEvent()。根据我的经验,这种职责的分离有助于更好的代码。
但最终,事件和可观察性都是很好的API,可以将事件推向世界,让订阅者可以随时收听和收听。我想说,可观察性是实现这一点的更现代的方式,在某些方面也更好,但事件更广泛,也更容易理解。因此,如果有什么东西是用来取代事件的,那么它就是可观察的。
推送<->拉力
值得注意的是,在紧急情况下,您可以在另一种方法之上构建任何一种方法:
- 要在pull之上构建push,请不断地从pull API中pull,然后将chunk推送给任何消费者
- 要在推送之上构建pull,请立即订阅推送API,创建一个累积所有结果的缓冲区,当有人进行拉取时,从该缓冲区中获取结果。(或者等到缓冲区变为非空,如果消费者的拉动速度快于API的包裹推送速度。)
后者通常比前者要编写更多的代码。
尝试在两者之间进行调整的另一个方面是,只有pull API才能轻松地传递背压。您可以添加一个侧通道来推送API,使它们能够将背压传递回源;我认为Dart做到了这一点,有些人试图创造具有这种能力的可观察性的进化。但IMO要比一开始就正确选择拉API更尴尬。另一方面,如果你使用推送API来暴露一个基本上基于拉的源,你将无法传达反压力。顺便说一句,这就是WebSocket和XMLHttpRequest API所犯的错误。
一般来说,我发现试图通过包装其他内容来将所有内容统一为一个API的做法是错误的。"推"one_answers"拉"有不同的、不太重叠的区域,它们都能很好地工作,说我们应该从你提到的四个API中选择一个并坚持使用,就像有些人所做的那样,这是短视的,会导致代码尴尬。
我对异步迭代程序的理解有点有限,但据我所知,WHATWG Streams是异步迭代程序中的一个特例。有关此方面的更多信息,请参阅Streams API常见问题解答。它简要介绍了与Observables的区别。
Async Iterator和Observables都是操作多个异步值的通用方法。目前,它们不进行互操作,但似乎正在考虑从异步迭代程序创建Observable。可观察的基于推送的特性与当前的事件系统非常相似,异步可观察的是基于拉的。一个简化的视图是:
-------------------------------------------------------------------------
| | Singular | Plural |
-------------------------------------------------------------------------
| Spatial (pull based) | Value | Iterable<Value> |
-------------------------------------------------------------------------
| Temporal (push based) | Promise<Value> | Observable<Value> |
-------------------------------------------------------------------------
| Temporal (pull based) | await on Promise | await on Iterable<Promise> |
-------------------------------------------------------------------------
我将AsyncIterables
表示为Iterable<Promise>
,以便于推理。请注意,await Iterable<Promise>
没有意义,因为它应该在for await...of AsyncIterator
循环中使用。
你可以找到一个更完整的解释Kriskowal:反应性的一般理论。