Observables 是多个值的惰性推送集合。它填补了下面表格中的空白
首先理解 pull 和 push。
pull直接理解为 使用者主动拿来用。
push理解为 数据发送者推送出去,使用者等待数据流动到,并且接受。
那么observable就是多个值的推送合集(异步序列sequence),或者直白粗暴理解,多个promise的集合。
前置知识:响应式编程 Reactive Programming
Event Buses、点击事件,http请求都是异步流。开发者可以观测这些异步流,并调用特定的逻辑对它们进行处理。使用Reactive如同开挂:你可以创建点击、悬停之类的任意流。通常流廉价(点击一下就出来一个)而无处不在,种类丰富多样:变量,用户输入,属性,缓存,数据结构等等都可以产生流。举例来说:微博回文(译者注:比如你关注的微博更新了)和点击事件都是流:你可以监听流并调用特定的逻辑对它们进行处理。
基于流的概念,RP赋予了你一系列神奇的函数工具集,使用他们可以合并、创建、过滤这些流。 一个流或者一系列流可以作为另一个流的输入。你可以 合并 多个流(经常使用的forkjoin操作符,相当于promise中的promiseall),从一堆流中 过滤 你真正感兴趣的那一些,将值从一个流 映射 到另一个流。
流(stream)是包含了 有时序(sequence),正在进行事件 的序列,可以发射(emmit)值(某种类型)、错误、完成信号。流在包含按钮的浏览器窗口被关闭时发出完成信号。
我们 异步地 捕获发射的事件,定义一系列函数在值被发射后,在错误被发射后,或在完成信号被发射后执行。有时,我们忽略对错误、完成信号地处理,仅仅关注对值的处理。对流进行监听,通常称为 订阅 ,处理流的函数是观测者observer,流是被观测的主体subject。这就是观察者设计模式。(这句话是重点概念)
--a---b-c---d---X---|-> a, b, c, d 是被发射的值 X 是一个错误 | 是 ‘完成’ 信号 ---> 是时间线
首先我们创建一个计数流来表明按钮被点击的次数。在RP中,每一个流都拥有一系列方法,例如map
,filter
,scan
等等。当你在流上调用这些方法,例如clickStream.map(f)
,会返回基于点击事件流的 新的流 ,同时原来的点击事件流并不会被改变,这个特性被称为 不可变性(immutability) 。不可变性与RP配合相得益彰。我们可以链式地调用他们:clickStream.map(f).scan(g)
:
clickStream: ---c----c--c----c------c-->
vvvvv map(c becomes 1) vvvv
---1----1--1----1------1-->
vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
map(f)
函数对原来的流使用我们提供的f函数
进行转换,并生成新的流。在上面的例子中,我们将每一次点击映射为数字1。scan(g)
函数将所有流产生的值进行汇总,通过传入x = g(accumulated, current)
函数产生新的值,g
是简单的求和函数。最后 counterStream
在点击发生后发射点击事件发生的总数。
那么Promise也是可观察对象吗?
是的!
可观察对象(Observable)是Promise++(原文Promise++,可以对比C,C++,C++在兼容C的同时引入了面向对象等特性)。 在Rx环境中,你可以简单的通过var stream = Rx.Observable.fromPromise(promise)
将Promise转换为可观察对象, 我们后面将这样使用, 唯一的区别是,可观察对象与Promises/A+ 并不兼容, 但是理论上不会产生冲突。 Promise 可以看做只能发射单值的可观察对象,Rx流则允许返回多个值。
两个场景来理解这两者的区别:
1. promise字面意思,承诺。 ‘我写完这段代码就去干一些别的事情’ 此时, ‘别的事情’ 可以是 ‘摸鱼’ 或者 ‘喝咖啡’ 获取其他事情
2.Rxjs是一段流(stream)。举个例子:我作为一个observer,我要去订阅subject报纸。 报社发报纸的过程是一个异步流,月底报纸印刷完成了是一个observable object. 那么此时报社知道我订阅过报纸,报纸就会被送到我家门口。observableObject.subject( observer )
关于使用RXJS的时机
1. 当你处理多个异步函数序列sequence。(这种情况下需要建立一个状态机函数来分别处理不同的异步结果。此时可以使用RXJS的API,主要是对于多个异步流的处理
2. 如果所涉及的应用程序/类库具有非常少的基于异步/基于事件的操作, 那就不要用rxjs,只用promise
var input = document.getElementById('input');
var dictionarySuggest = Rx.Observable.fromEvent(input, 'keyup')
.map(() => input.value)
.filter(text => !!text)
.distinctUntilChanged()
.throttle(250)
.flatMapLatest(searchWikipedia)
.subscribe(
results => {
list = [];
list.concat(results.map(createItem));
},
err => logError(err)
);
此示例模拟在用户键入时,接收输入建议数据(autocomplete)的常见UI范例。
RxJS创建一个可观察序列,对输入的现有keyup事件建模。
然后它在事件后放置了几个过滤器和映射,让事件只有在发生了值的变化时才触发。 (每次按键都会触发 keyup 事件,包括用户按左右箭头移动光标,但这时输入文本并不会变化)。
接下来通过使用 throttle 操作符确保事件只在250毫秒的没有输入时才被触发。 (如果用户仍在键入字符,会被立即延迟请求,从而减少一次潜在的昂贵的请求)。
在以前编写的程序中,这种节流将通过定时器引入单独的回调。 这个计时器可能会抛出异常(某些定时器在运行中可能有大量的操作)。
一旦用户输入被过滤掉,就是执行查找的时间。 由于这通常是耗时的操作(例如对在世界的另一侧的服务器的请求),所以该操作本身也是异步的。
flatMap/selectMany 操作符可以方便地组合多个异步操作。它不仅可以合并请求成功后返回的值;它还可以跟踪在每个单独操作中发生的任何异常。
在传统的编程中,这将引入单独的回调和异常捕获。
如果用户在查询操作仍在进行时键入了新字符,我们不再需要看到该查询的结果。 因为用户已经键入更具体的词,看到旧的结果将会非常困惑。
flatMapLatest
操作确保一旦检测到新的keyup
,就忽略查询操作。最后,我们订阅所得的可观察序列。 只有在这个时候我们的回调函数将被调用。 我们传递两个函数到
subscribe
调用:
- 从我们的计算接收结果。
- 在执行的任何地方发生故障的情况下捕获异常。
2022/7/21
参考表:
静态方法
实例操作符
使用现有的序列 | 我想改变每个值 | map/select | ||
我想从每个值拉一个属性 | pluck | |||
我想在不影响值的情况下被通知值 | do/tap doOnNext/tapOnNext doOnError/tapOnError doOnCompleted/tapOnCompleted | |||
我想包含值 | 基于自定义逻辑 | filter/where | ||
从序列开头 | take | |||
基于自定义逻辑 | takeWhile | |||
从序列的末尾 | takeLast | |||
直到另一个序列发射一个值或完成 | takeUntil | |||
我想忽略值 | 全部 | ignoreElements | ||
从序列的开头 | skip | |||
基于自定义逻辑 | skipWhile | |||
从序列的末尾 | skipLast | |||
直到另一个序列发出一个值 | skipUntil | |||
与以前的值相同 | distinctUntilChanged | |||
这(触发)太频繁 | throttle | |||
我想计算 | 总和 | 这些值的 | sum | |
平均值 | average | |||
使用自定义逻辑 | 并且只输出最终值 | aggregate reduce | ||
并在计算出值时输出(每一步的)值 | scan | |||
我想用元数据包装它的消息 | 描述每个消息 | materialize | ||
包括从最后一个价值以来的时间 | timeInterval | |||
包括时间戳 | timestamp | |||
经过一段时间的不活动 | 我想抛出一个错误 | timeout | ||
我想切换到另一个序列 | timeout | |||
我想确保只有一个值 | 并且如果存在多于或少于一个值则抛出错误 | single | ||
并且如果没有值,则使用默认值 | singleOrDefault | |||
我只想取第一个值 | 并且如果没有值,则抛出错误 | first | ||
并且如果没有值,则使用默认值 | firstOrDefault | |||
在一段时间内 | sample | |||
我只想取最后的值 | 如果没有值,则报错 | last | ||
并且如果没有值,则使用默认值 | lastOrDefault | |||
我想知道它包含多少值 | count | |||
我想知道它是否包含一个指定的值 | contains | |||
我想知道条件是否满足 | 只需要任一值满足 | any/some | ||
需要所有值都满足 | all/every | |||
我想把消息延迟一段特定的时间 | delay | |||
基于自定义逻辑 | delayWithSelector | |||
我想给值分组 | 直到序列完成 | toArray toMap toSet | ||
使用自定义逻辑 | 作为数组 | buffer | ||
作为序列 | window | |||
根据特定大小分批 | 作为数组 | bufferWithCount | ||
作为序列 | windowWithCount | |||
基于时间 | 作为数组 | bufferWithTime | ||
作为序列 | windowWithTime | |||
基于时间或计数,以先发生者为准 | 作为数组 | bufferWithTimeOrCount | ||
作为序列 | windowWithTimeOrCount | |||
基于一个指定的key | 直到序列完成 | groupBy | ||
并控制每组的生命周期 | groupByUntil | |||
我想为每个值开始一个新的序列 | 并且并行地从所有序列中发出值 | flatMap/selectMany | ||
并按顺序从每个序列中输出值 | concatMap/selectConcat | |||
并在新值到达时取消先前的序列 | flatMapLatest/selectSwitch | |||
并递归地为每个新值启动一个新的序列 | expand | |||
并根据onNext,onError和onCompleted并行地从所有序列发出值 | flatMapObserver/selectManyObserver | |||
并根据onNext,onError和onCompleted顺序地从所有序列发出值 | concatMapObserver/selectConcatObserver | |||
我想把它与另一个结合起来 | 两者都完成时发出通知 | forkJoin | ||
我想执行复杂的操作,而不会打破流畅的调用 | let | |||
我想在多个订阅者之间共享订阅 | 使用特定的`subject`实现 | multicast | ||
publish share | ||||
并向未来订阅者提供最后的值 | publishLast shareLast | |||
并向未来订阅者重播默认值或最新值 | publishValue shareValue | |||
并向未来的订阅者重播n个值 | replay shareReplay | |||
发生错误时 | 我想重新订阅 | retry | ||
我想开始一个新序列 | catch | |||
取决于错误 | catch | |||
当完成时 | 我想重新订阅 | repeat | ||
我想开始一个新序列 | concat | |||
当完成或抛出错误时 | 我想开始一个新序列 | onErrorResumeNext | ||
当完成,抛出错误或退订时 | 我想执行一个函数 | finally | ||
我想改变路由的调度程序 | 调用`subscribe`(订阅) | subscribeOn | ||
消息 | observeOn | |||
使用两个序列 | 我想决定从哪个接收值 | 取决于哪个序列先发出值 | amb | |
我想确定它们的值是否相等 | sequenceEqual | |||
我想合并它们的值 | 只有当第一个序列发射时,使用每个序列的最新值 | withLatestFrom | ||
为了 | 不改变时重复使用最新值 | combineLatest | ||
每个值只使用一次 | zip | |||
重复分享我选择的“生命周期” | 并通知每个组合 | join | ||
并给每个“左”的序列的值给“右”的序列 | groupJoin | |||
我想包含两者的值 | merge |
reference:
附:
编程范式:命令式编程、声明式编程、函数式编程和响应式编程。
命令式编程:命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。
//1. 声明变量
List<int> results = new List<int>();
//2. 循环变量
foreach(var num in Enumerable.Range(1,10))
{
//3. 添加条件
if (num > 5)
{
//4. 添加处理逻辑
results.Add(num);
Console.WriteLine(num);
}
}
声明式编程:声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。
var nums = from num in Enumerable.Range(1,10) where num > 5 select num
函数式编程:主要思想是把运算过程尽量写成一系列嵌套的函数调用。
Enumerable.Range(1, 10).Where(num => num > 5).ToList().ForEach(Console.WriteLine);
响应式编程:响应式编程是一种面向数据流和变化传播的编程范式,旨在简化事件驱动应用的实现。响应式编程专注于如何创建依赖于变更的数据流并对变化做出响应。
IObservable<int> nums = Enumerable.Range(1, 10).ToObservable();
IDisposable subscription = nums.Where(num => num > 5).Subscribe(Console.WriteLine);
subscription.Dispose();