事件 vs 流 vs 可观察对象 vs 异步迭代器

Events vs Streams vs Observables vs Async Iterators

目前,在JavaScript中处理一系列异步结果的唯一稳定方法是使用事件系统。但是,正在开发三种替代方案:

流:https://streams.spec.whatwg.org
观察值:https://tc39.github.io/proposal-observable
异步迭代器:https://tc39.github.io/proposal-async-iteration

每个事件和其他事件的区别和好处是什么?

这些是否有意取代事件?

我对异步迭代器的理解有点有限,但据我了解,WHATWG Streams 是异步迭代器的一个特例。有关这方面的更多信息,请参阅 Streams API FAQ. It briefly addresses how differs from Observables.

Async Iterators 和 Observables 都是操作多个异步值的通用方法。现在他们不互操作,但似乎正在考虑创建 Observables from Async Iterators。 基于推送的 Observables 更类似于当前的事件系统,AsyncIterables 是基于拉取的。一个简化的视图是:

-------------------------------------------------------------------------    
|                       | 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: A General Theory of Reactivity

这里的API大致分为两类:拉和推。

异步拉取 APIs 非常适合从源中拉取数据的情况。这个源可能是一个文件,或者一个网络套接字,或者一个目录列表,或者其他任何东西。关键是在被要求时完成从源中提取或生成数据的工作。

异步迭代器是这里的基本原语,意味着是基于拉的异步源概念的一般表现。在这样的来源中,您:

  • 通过执行 const promise = ai.next()
  • 从异步迭代器中提取
  • 使用const result = await promise(或使用.then())等待结果
  • 检查结果以确定它是异常(抛出)、中间值({ value, done: false }) 还是完成信号({ value: undefined, done: true })。

这类似于同步迭代器是基于拉取的同步值源概念的一般表现形式。同步迭代器的步骤与上面完全相同,省略了"wait for the result"步骤。

可读流是异步迭代器的一种特殊情况,旨在专门封装 I/O 源,如 sockets/files/etc。他们有专门的 API 用于将它们通过管道传输到可写流(代表 I/O 生态系统的另一半,接收器)并处理由此产生的背压。它们还可以专门用于以高效 "bring your own buffer" 方式处理字节。这有点让人想起数组是同步迭代器的一种特殊情况,针对 O(1) 索引访问进行了优化。

pull APIs还有一个特点就是一般都是单消费者。谁拉取了值,现在就拥有了它,并且它不存在于源异步 iterator/stream/etc 中。了。已经被消费拉走了

一般来说,pull APIs 提供了一个与一些底层数据源通信的接口,允许消费者表达对它的兴趣。这与...

推送

Push APIs 非常适合在某些东西正在生成数据时,并且正在生成的数据不关心是否有人需要它。例如,不管某人是否感兴趣,你的鼠标移动了,然后你点击了某个地方,这仍然是真的。您希望通过推送 API 来证明这些事实。然后,消费者——可能是多个消费者——可以订阅,以推送有关此类事件发生的通知。

API 本身并不关心是零个、一个还是多个消费者订阅。它只是表明宇宙中发生的事情的一个事实。

事件是这一点的简单体现。您可以在浏览器中订阅一个 EventTarget,或在 Node.js 中订阅 EventEmitter,并获得已调度事件的通知。 (通常,但不总是,由 EventTarget 的创建者。)

Observables 是 EventTarget 的更完善的版本。他们的主要创新是订阅本身由第一个 class 对象表示,即 Observable,然后您可以在其上应用组合器(例如过滤器、地图等)。他们还选择将三个信号(通常命名为 next、complete 和 error)捆绑在一起,并赋予这些信号特殊的语义,以便组合器尊重它们。这与 EventTarget 相反,其中事件名称没有特殊语义(EventTarget 的任何方法都不会关心您的事件是命名为 "complete" 还是命名为 "asdf")。 Node 中的 EventEmitter 具有这种特殊语义方法的某些版本,其中 "error" 事件可能会使进程崩溃,但这是相当原始的。

Observables over events 的另一个很好的特性是,通常只有 Observables 的创建者才能使其产生那些 next/error/complete 信号。而在 EventTarget 上,任何人都可以调用 dispatchEvent()。根据我的经验,这种职责分离有助于编写更好的代码。

但最终,事件和可观察对象都是很好的 API 将事件推向世界,提供给可以随时调入和调出的订阅者。我想说 observables 是更现代的方式来做到这一点,并且在某些方面更好,但事件更广泛和更容易理解。因此,如果有任何东西旨在取代事件,那将是可观察的。

推 <-> 拉

值得注意的是,您可以在紧要关头构建任何一种方法:

  • 要在拉取之上构建推送,不断从拉取中拉取 API,然后将块推送给任何消费者。
  • 要在推送之上构建拉取,立即订阅推送 API,创建一个累积所有结果的缓冲区,当有人拉取时,从该缓冲区中获取它。 (或者等到缓冲区变为非空,如果您的消费者拉动速度快于包裹推送 API 推送。)

后者通常比前者要编写的代码多得多。

尝试在两者之间进行适配的另一个方面是,只有拉 APIs 才能轻松传达背压。您可以添加一个侧通道来推送 APIs 以允许它们将背压传回源;我认为 Dart 做到了这一点,有些人试图创造具有这种能力的可观察对象的演变。但在我看来,这比一开始就正确选择 pull API 要尴尬得多。不利的一面是,如果您使用推送 API 来公开基于拉的源,您将无法传达背压。顺便说一句,这是 WebSocket 和 XMLHttpRequest APIs 所犯的错误。

总的来说,我发现通过包装其他人来将所有内容统一为一个 API 的尝试是错误的。推和拉有不同的、不太重叠的区域,它们各自工作得很好,并且说我们应该从你提到的四个 API 中选择一个并坚持下去,就像一些人所做的那样,是短视的并且导致到笨拙的代码。