RxJS笔记
参考资料:
概述
简介
RxJS 是用于 JavaScript 的 ReactiveX 库。ReactiveX是一种针对异步数据流的编程。简单来说,它将一切数据,包括HTTP请求,DOM事件或者普通数据等包装成流的形式,然后用强大丰富的操作符对流进行处理,使你能以同步编程的方式处理异步数据,并组合不同的操作符来轻松优雅的实现你所需要的功能。
ReactiveX 结合了 观察者模式、迭代器模式 和 使用集合的函数式编程,以满足以一种理想方式来管理事件序列所需要的一切。
RxJS 通过使用 observable 序列来编写异步和基于事件的程序。它提供了一个核心类型 Observable,附属类型 (Observer、 Schedulers、 Subjects) 和受 [Array#extras] 启发的操作符 (map、filter、reduce、every, 等等),这些数组操作符可以把异步事件作为集合来处理。
可以把 RxJS 当做是用来处理事件的 Lodash 。
函数式编程和响应式编程
RxJS 引入了两种重要的编程思想:函数式编程和响应式编程。
函数式编程
函数式编程(Functional Programming,简称 FP)是一种编程范式,强调使用函数来思考问题、编写代码。
函数式编程的主要设计点在于避免使用状态和可变的数据,即 stateless and immutable。
函数式编程对函数的使用有一些特殊要求:
-
声明式(Declarative)
声明式的函数,让开发者只需要表达”想要做什么”,而不需要表达“怎么去做”。
-
纯函数(Pure Function)
纯函数指的是执行结果由输入参数决定,参数相同时结果相同,不受其他数据影响,并且不会带来副作用(Side Effect)的函数。副作用指的是函数做了和本身运算返回值没有关系的事情,如修改外部变量或传入的参数对象,甚至是执行 console.log 都算是 Side Effect。前端中常见的副作用有发送 http 请求、操作 DOM、调用 alert 或者 confirm 函数等。满足纯函数的特性也叫做引用透明度(Referential Transparency)。
-
数据不可变性(Immutability)
数据不可变就是指这个数据一旦产生,它的值就永远不会变。JavaScript 中字符串类型和数字类型就是不可改变的,而对象基本都是可变的,可能会带来各种副作用。现在有各种库可以实现 Immutable 特性,如 immutable.js 和 immer.js
响应式编程
响应式编程(Reactive Programming)是一种面向数据流(stream)和变化传播的编程范式,使用各种函数创建、组合、过滤数据流,然后通过监听这个数据流来响应它的变化。响应式编程抽象出了流这个概念,提高了代码的抽象级别,我们不用去关心大量的实现细节,而专注于对数据流的操作。
响应式流可以认为是随着时间发出的一系列元素。响应式和观察者模式有点相似,订阅者订阅后,发布者吐出数据时,订阅者会响应式进行处理。
为什么使用RxJS
为什么使用RxJS?这需要从异步说起。前端编程中的异步有:事件(event)、AJAX、动画(animation)、定时器(timer)。
异步常见的问题
-
回调地狱(Callback Hell)
回调地狱就是指层层嵌套的回调函数,造成代码难以理解,并且难以协调组织复杂的操作。
-
竞态条件(Race Condition)
竞态条件出现的原因是无法保证异步操作的完成会和他们开始时的顺序一样,因此最终结果不可控。比如常见的 AutoComplete 效果,每次输入后向后端发送请求获取结果展示在搜索框下面,由于网络、后端数据查询等原因有可能出现最后发送的请求比之前的请求更快地完成了,这时最终展现的并不是最后那个请求的结果,而这并不是我们所希望的。
-
内存泄漏(Memory Leak)
这里说的内存泄漏指的是单页应用切换页面时由于忘记在合适的时机移除监听事件造成的内存泄漏。
-
管理复杂状态(Manage Complex States)
异步带来了状态的改变,可能会使状态管理变得非常复杂,尤其是某个状态有多个来源时,比如有些应用,一开始有一个默认值,再通过 AJAX 获取初始状态,存储在 localStorage,之后通过 WebSocket 获取更新。这时查询状态可能是同步或者异步的,状态的变更可能是主动获取也可能是被动推送的,如果还有各种排序、筛选,状态管理将会更加复杂。
-
错误处理(Exception Handling)
JavaScript 中的 try/catch 只能捕获同步的错误,异步的错误不易处理。
Promise 的缺点
使用 Promise 可以减轻一些异步问题,如将回调函数变为串行的链式调用,统一同步和异步代码等,async/await 中也可以使用 try/catch 来捕获错误。但是对于复杂的场景,仍然难于处理。而且 Promise 还有其他的问题,一是只有一个结果,二是会立即开始执行且不可以取消。
异步 API
异步编程时不仅要面对上述问题,还有下面这些使用方式各异的 API:
- DOM Events
- XMLHttpRequest
- fetch
- WebSocket
- Service Worker
- setTimeout
- setInterval
- requestAnimationFrame
而如果使用 RxJS,可以用统一的 API 来进行处理,而且借助 RxJS 各种强大的操作符,我们可以更简单地实现我们的需求。
安装和引入
通过 npm 安装 ES6 版本
npm install rxjs
导入整个核心功能集:
import Rx from 'rxjs/Rx';
// use example
Rx.Observable.of(1,2,3)
适用场景
不要过度使用 Rx,它比较适合以下场景:
-
组合事件时
-
增加延迟和控制频率
-
组合异步任务
-
需要取消时
简单的应用并不需要 RxJS。
入门示例
事件监听
注册事件监听器的常规写法。
var button = document.querySelector('button');
button.addEventListener('click', () => console.log('Clicked!'));
使用 RxJS 的话,创建一个 observable 来代替。
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.subscribe(() => console.log('Clicked!'));
纯净性 (Purity)
使得 RxJS 强大的正是它使用纯函数来产生值的能力。这意味着你的代码更不容易出错。
通常你会创建一个非纯函数,在这个函数之外也使用了共享变量的代码,这将使得你的应用状态一团糟。
var count = 0;
var button = document.querySelector('button');
button.addEventListener('click', () => console.log(`Clicked ${++count} times`));
使用 RxJS 的话,你会将应用状态隔离出来。
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.scan(count => count + 1, 0)
.subscribe(count => console.log(`Clicked ${count} times`));
scan 操作符的工作原理与数组的 reduce 类似。它需要一个暴露给回调函数当参数的初始值。每次回调函数运行后的返回值会作为下次回调函数运行时的参数。
流动性 (Flow)
RxJS 提供了一整套操作符来帮助你控制事件如何流经 observables 。
下面的代码展示的是如何控制一秒钟内最多点击一次,先来看使用普通的 JavaScript:
var count = 0;
var rate = 1000;
var lastClick = Date.now() - rate;
var button = document.querySelector('button');
button.addEventListener('click', () => {
if (Date.now() - lastClick >= rate) {
console.log(`Clicked ${++count} times`);
lastClick = Date.now();
}
});
使用 RxJS:
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.throttleTime(1000)
.scan(count => count + 1, 0)
.subscribe(count => console.log(`Clicked ${count} times`));
值 (Values)
对于流经 observables 的值,你可以对其进行转换。
下面的代码展示的是如何累加每次点击的鼠标 x 坐标,先来看使用普通的 JavaScript:
var count = 0;
var rate = 1000;
var lastClick = Date.now() - rate;
var button = document.querySelector('button');
button.addEventListener('click', (event) => {
if (Date.now() - lastClick >= rate) {
count += event.clientX;
console.log(count);
lastClick = Date.now();
}
});
使用 RxJS:
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.throttleTime(1000)
.map(event => event.clientX)
.scan((count, clientX) => count + clientX, 0)
.subscribe(count => console.log(count));
基本概念
在 RxJS 中用来解决异步事件管理的的基本概念是:
- Observable (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。只出不进
- Observer (观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。只进不出
- Subscription (订阅): 表示 Observable 的执行,主要用于取消 Observable 的执行。
- Operators (操作符): 采用函数式编程风格的纯函数 (pure function),使用像
map
、filter
、concat
、flatMap
等这样的操作符来处理集合。 - Subject (主体): 相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。可出可进的可观察序列,可作为观察者
- Schedulers (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如
setTimeout
或requestAnimationFrame
或其他。
Observable (可观察对象)
Observables 是多个值的惰性推送集合。 它填补了下面表格中的空白:
单个值 | 多个值 | |
---|---|---|
拉取 | Function | Iterator |
推送 | Promise | Observable |
拉取 (Pull) vs. 推送 (Push)
拉取和推送是两种不同的协议,用来描述数据**生产者 (Producer)如何与数据消费者 (Consumer)**进行通信的。
什么是拉取? - 在拉取体系中,由消费者来决定何时从生产者那里接收数据。生产者本身不知道数据是何时交付到消费者手中的。
每个 JavaScript 函数都是拉取体系。函数是数据的生产者,调用该函数的代码通过从函数调用中“取出”一个单个返回值来对该函数进行消费。
ES2015 引入了 generator 函数和 iterators (function*
),这是另外一种类型的拉取体系。调用 iterator.next()
的代码是消费者,它会从 iterator(生产者) 那“取出”多个值。
生产者 | 消费者 | |
---|---|---|
拉取 | 被动的: 当被请求时产生数据。 | 主动的: 决定何时请求数据。 |
推送 | 主动的: 按自己的节奏产生数据。 | 被动的: 对收到的数据做出反应。 |
什么是推送? - 在推送体系中,由生产者来决定何时把数据发送给消费者。消费者本身不知道何时会接收到数据。
在当今的 JavaScript 世界中,Promises 是最常见的推送体系类型。Promise(生产者) 将一个解析过的值传递给已注册的回调函数(消费者),但不同于函数的是,由 Promise 来决定何时把值“推送”给回调函数。
RxJS 引入了 Observables,一个新的 JavaScript 推送体系。Observable 是多个值的生产者,并将值“推送”给观察者(消费者)。
小结:
- Function 是惰性的评估运算,调用时会同步地返回一个单一值。
- Generator 是惰性的评估运算,调用时会同步地返回零到(有可能的)无限多个值。
- Promise 是最终可能(或可能不)返回单个值的运算。
- Observable 是惰性的评估运算,它可以从它被调用的时刻起同步或异步地返回零到(有可能的)无限多个值。
Observables 作为函数的泛化
与流行的说法正好相反,Observables 既不像 EventEmitters,也不像多个值的 Promises 。在某些情况下,即当使用 RxJS 的 Subjects 进行多播时, Observables 的行为可能会比较像 EventEmitters,但通常情况下 Observables 的行为并不像 EventEmitters 。
Observables 像是没有参数, 但可以泛化为多个值的函数。
考虑如下代码:
function foo() {
console.log('Hello');
return 42;
}
var x = foo.call(); // 等同于 foo()
console.log(x);
var y = foo.call(); // 等同于 foo()
console.log(y);
我们期待看到的输出:
"Hello"
42
"Hello"
42
你可以使用 Observables 重写上面的代码:
var foo = Rx.Observable.create(function (observer) {
console.log('Hello');
observer.next(42);
});
foo.subscribe(function (x) {
console.log(x);
});
foo.subscribe(function (y) {
console.log(y);
});
输出是一样的:
"Hello"
42
"Hello"
42
这是因为函数和 Observables 都是惰性运算。如果你不调用函数,console.log('Hello')
就不会执行。Observables 也是如此,如果你不“调用”它(使用 subscribe
),console.log('Hello')
也不会执行。此外,“调用”或“订阅”是独立的操作:两个函数调用会触发两个单独的副作用,两个 Observable 订阅同样也是触发两个单独的副作用。EventEmitters 共享副作用并且无论是否存在订阅者都会尽早执行,Observables 与之相反,不会共享副作用并且是延迟执行。
订阅 Observable 类似于调用函数。
一些人声称 Observables 是异步的。那不是真的。如果你用日志包围一个函数调用,像这样:
console.log('before');
console.log(foo.call());
console.log('after');
你会看到这样的输出:
"before"
"Hello"
42
"after"
使用 Observables 来做同样的事:
console.log('before');
foo.subscribe(function (x) {
console.log(x);
});
console.log('after');
输出是:
"before"
"Hello"
42
"after"
这证明了 foo
的订阅完全是同步的,就像函数一样。
Observables 传递值可以是同步的,也可以是异步的。
那么 Observable 和 函数的区别是什么呢?Observable 可以随着时间的推移“返回”多个值,这是函数所做不到的。你无法这样:
function foo() {
console.log('Hello');
return 42;
return 100; // 死代码,永远不会执行
}
函数只能返回一个值。但 Observables 可以这样:
var foo = Rx.Observable.create(function (observer) {
console.log('Hello');
observer.next(42);
observer.next(100); // “返回”另外一个值
observer.next(200); // 还可以再“返回”值
});
console.log('before');
foo.subscribe(function (x) {
console.log(x);
});
console.log('after');
同步输出:
"before"
"Hello"
42
100
200
"after"
但你也可以异步地“返回”值:
var foo = Rx.Observable.create(function (observer) {
console.log('Hello');
observer.next(42);
observer.next(100);
observer.next(200);
setTimeout(() => {
observer.next(300); // 异步执行
}, 1000);
});
console.log('before');
foo.subscribe(function (x) {
console.log(x);
});
console.log('after');
输出:
"before"
"Hello"
42
100
200
"after"
300
结论:
func.call()
意思是 “同步地给我一个值”observable.subscribe()
意思是 “给我任意数量的值,无论是同步还是异步”
Observable 剖析
Observables 是使用 Rx.Observable.create
或创建操作符创建的,并使用观察者来订阅它,然后执行它并发送 next
/ error
/ complete
通知给观察者,而且执行可能会被清理。这四个方面全部编码在 Observables 实例中,但某些方面是与其他类型相关的,像 Observer (观察者) 和 Subscription (订阅)。
Observable 的核心关注点:
- 创建 Observables
- 订阅 Observables
- 执行 Observables
- 清理 Observables
创建 Observables
Rx.Observable.create
是 Observable
构造函数的别名,它接收一个参数:subscribe
函数。
下面的示例创建了一个 Observable,它每隔一秒会向观察者发送字符串 'hi'
。
var observable = Rx.Observable.create(function subscribe(observer) {
var id = setInterval(() => {
observer.next('hi')
}, 1000);
});
Observables 可以使用 create
来创建, 但通常我们使用所谓的创建操作符, 像 of
、from
、interval
、等等。
在上面的示例中,subscribe
函数是用来描述 Observable 最重要的一块。
订阅 Observables
示例中的 Observable 对象 observable
可以订阅,像这样:
observable.subscribe(x => console.log(x));
observable.subscribe
和 Observable.create(function subscribe(observer) {...})
中的 subscribe
有着同样的名字,这并不是一个巧合。在库中,它们是不同的,但从实际出发,你可以认为在概念上它们是等同的。
这表明 subscribe
调用在同一 Observable 的多个观察者之间是不共享的。当使用一个观察者调用 observable.subscribe
时,Observable.create(function subscribe(observer) {...})
中的 subscribe
函数只服务于给定的观察者。对 observable.subscribe
的每次调用都会触发针对给定观察者的独立设置。
订阅 Observable 像是调用函数, 并提供接收数据的回调函数。
这与像 addEventListener
/ removeEventListener
这样的事件处理方法 API 是完全不同的。使用 observable.subscribe
,在 Observable 中不会将给定的观察者注册为监听器。Observable 甚至不会去维护一个附加的观察者列表。
subscribe
调用是启动 “Observable 执行”的一种简单方式, 并将值或事件传递给本次执行的观察者。
执行 Observables
Observable.create(function subscribe(observer) {...})
中...
的代码表示 “Observable 执行”,它是惰性运算,只有在每个观察者订阅后才会执行。随着时间的推移,执行会以同步或异步的方式产生多个值。
Observable 执行可以传递三种类型的值:
- “Next” 通知: 发送一个值,比如数字、字符串、对象,等等。
- “Error” 通知: 发送一个 JavaScript 错误 或 异常。
- “Complete” 通知: 不再发送任何值。
“Next” 通知是最重要,也是最常见的类型:它们表示传递给观察者的实际数据。“Error” 和 “Complete” 通知可能只会在 Observable 执行期间发生一次,并且只会执行其中的一个。
这些约束用所谓的 _Observable 语法_或_合约_表达最好,写为正则表达式是这样的:
next*(error|complete)?
在 Observable 执行中, 可能会发送零个到无穷多个 “Next” 通知。如果发送的是 “Error” 或 “Complete” 通知的话,那么之后不会再发送任何通知了。
下面是 Observable 执行的示例,它发送了三个 “Next” 通知,然后是 “Complete” 通知:
var observable = Rx.Observable.create(function subscribe(observer) {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
Observable 严格遵守自身的规约,所以下面的代码不会发送 “Next” 通知 4
:
var observable = Rx.Observable.create(function subscribe(observer) {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
observer.next(4); // 因为违反规约,所以不会发送
});
在 subscribe
中用 try
/catch
代码块来包裹任意代码是个不错的主意,如果捕获到异常的话,会发送 “Error” 通知:
var observable = Rx.Observable.create(function subscribe(observer) {
try {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
} catch (err) {
observer.error(err); // 如果捕获到异常会发送一个错误
}
});
清理 Observable 执行
因为 Observable 执行可能会是无限的,并且观察者通常希望能在有限的时间内中止执行,所以我们需要一个 API 来取消执行。因为每个执行都是其对应观察者专属的,一旦观察者完成接收值,它必须要一种方法来停止执行,以避免浪费计算能力或内存资源。
当调用了 observable.subscribe
,观察者会被附加到新创建的 Observable 执行中。这个调用还返回一个对象,即 Subscription
(订阅):
var subscription = observable.subscribe(x => console.log(x));
Subscription 表示进行中的执行,它有最小化的 API 以允许你取消执行。使用 subscription.unsubscribe()
你可以取消进行中的执行:
var observable = Rx.Observable.from([10, 20, 30]);
var subscription = observable.subscribe(x => console.log(x));
// 稍后:
subscription.unsubscribe();
当你订阅了 Observable,你会得到一个 Subscription ,它表示进行中的执行。只要调用 unsubscribe()
方法就可以取消执行。
当我们使用 create()
方法创建 Observable 时,Observable 必须定义如何清理执行的资源。你可以通过在 function subscribe()
中返回一个自定义的 unsubscribe
函数。
举例来说,这是我们如何清理使用了 setInterval
的 interval 执行集合:
var observable = Rx.Observable.create(function subscribe(observer) {
// 追踪 interval 资源
var intervalID = setInterval(() => {
observer.next('hi');
}, 1000);
// 提供取消和清理 interval 资源的方法
return function unsubscribe() {
clearInterval(intervalID);
};
});
正如 observable.subscribe
类似于 Observable.create(function subscribe() {...})
,从 subscribe
返回的 unsubscribe
在概念上也等同于 subscription.unsubscribe
。事实上,如果我们抛开围绕这些概念的 ReactiveX 类型,保留下来的只是相当简单的 JavaScript 。
function subscribe(observer) {
var intervalID = setInterval(() => {
observer.next('hi');
}, 1000);
return function unsubscribe() {
clearInterval(intervalID);
};
}
var unsubscribe = subscribe({next: (x) => console.log(x)});
// 稍后:
unsubscribe(); // 清理资源
为什么我们要使用像 Observable、Observer 和 Subscription 这样的 Rx 类型?原因是保证代码的安全性(比如 Observable 规约)和操作符的可组合性。
Observer (观察者)
观察者 是由 Observable 发送的值的消费者。观察者只是一组回调函数的集合,每个回调函数对应一种 Observable 发送的通知类型:next
、error
和 complete
。
-
next: 当 Observable 发出新的值时被调用,接收这个值作为参数。
-
complete:当 Observable 完结,没有更多数据时被调用。complete 之后,next 方法无效。
-
error:当 Observable 内部发生错误时被调用,之后不会调用 complete,next 方法无效。
下面的示例是一个典型的观察者对象:
var observer = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
要使用观察者,需要把它提供给 Observable 的 subscribe
方法:
observable.subscribe(observer);
RxJS 中的观察者也可能是_部分的_。如果你没有提供某个回调函数,Observable 的执行也会正常运行,只是某些通知类型会被忽略,因为观察者中没有相对应的回调函数。
下面的示例是没有 complete
回调函数的观察者:
var observer = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
};
当订阅 Observable 时,你可能只提供了一个回调函数作为参数,而并没有将其附加到观察者对象上,例如这样:
observable.subscribe(x => console.log('Observer got a next value: ' + x));
在 observable.subscribe
内部,它会创建一个观察者对象并使用第一个回调函数参数作为 next
的处理方法。三种类型的回调函数都可以直接作为参数来提供:
observable.subscribe(
x => console.log('Observer got a next value: ' + x),
err => console.error('Observer got an error: ' + err),
() => console.log('Observer got a complete notification')
);
Subscription (订阅)
什么是 Subscription ? - Subscription 是表示可清理资源的对象,通常是 Observable 的执行。Subscription 有一个重要的方法,即 unsubscribe
,它不需要任何参数,只是用来清理由 Subscription 占用的资源。在上一个版本的 RxJS 中,Subscription 叫做 “Disposable” (可清理对象)。
var observable = Rx.Observable.interval(1000);
var subscription = observable.subscribe(x => console.log(x));
// 稍后:
// 这会取消正在进行中的 Observable 执行
// Observable 执行是通过使用观察者调用 subscribe 方法启动的
subscription.unsubscribe();
Subscription 基本上只有一个 unsubscribe()
函数,这个函数用来释放资源或去取消 Observable 执行。
Subscription 还可以合在一起,这样一个 Subscription 调用 unsubscribe()
方法,可能会有多个 Subscription 取消订阅 。你可以通过把一个 Subscription 添加到另一个上面来做这件事:
var observable1 = Rx.Observable.interval(400);
var observable2 = Rx.Observable.interval(300);
var subscription = observable1.subscribe(x => console.log('first: ' + x));
var childSubscription = observable2.subscribe(x => console.log('second: ' + x));
subscription.add(childSubscription);
setTimeout(() => {
// subscription 和 childSubscription 都会取消订阅
subscription.unsubscribe();
}, 1000);
执行时,我们在控制台中看到:
second: 0
first: 0
second: 1
first: 1
second: 2
Subscriptions 还有一个 remove(otherSubscription)
方法,用来撤销一个已添加的子 Subscription 。
Subject (主体)
Subject 是一种特殊类型的 Observable,它允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。
Subject 像是 Observable,但是可以多播给多个观察者。Subject 还像是 EventEmitters,维护着多个监听器的注册表。
每个 Subject 都是 Observable 。 - 对于 Subject,你可以提供一个观察者并使用 subscribe
方法,就可以开始正常接收值。从观察者的角度而言,它无法判断 Observable 执行是来自普通的 Observable 还是 Subject 。
在 Subject 的内部,subscribe
不会调用发送值的新执行。它只是将给定的观察者注册到观察者列表中,类似于其他库或语言中的 addListener
的工作方式。
每个 Subject 都是观察者。 - Subject 是一个有如下方法的对象: next(v)
、error(e)
和 complete()
。要给 Subject 提供新值,只要调用 next(theValue)
,它会将值多播给已注册监听该 Subject 的观察者们。
在下面的示例中,我们为 Subject 添加了两个观察者,然后给 Subject 提供一些值:
var subject = new Rx.Subject();
subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});
subject.next(1);
subject.next(2);
下面是控制台的输出:
observerA: 1
observerB: 1
observerA: 2
observerB: 2
因为 Subject 是观察者,这也就在意味着你可以把 Subject 作为参数传给任何 Observable 的 subscribe
方法,如下面的示例所展示的:
var subject = new Rx.Subject();
subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});
var observable = Rx.Observable.from([1, 2, 3]);
observable.subscribe(subject); // 你可以提供一个 Subject 进行订阅
执行结果:
observerA: 1
observerB: 1
observerA: 2
observerB: 2
observerA: 3
observerB: 3
使用上面的方法,我们基本上只是通过 Subject 将单播的 Observable 执行转换为多播的。这也说明了 Subjects 是将任意 Observable 执行共享给多个观察者的唯一方式。
还有一些特殊类型的 Subject
:BehaviorSubject
、ReplaySubject
和 AsyncSubject
。
BehaviorSubject
Subject 的其中一个变体就是 BehaviorSubject
,它有一个“当前值”的概念。它保存了发送给消费者的最新值。并且当有新的观察者订阅时,会立即从 BehaviorSubject
那接收到“当前值”。
BehaviorSubject 适合用来表示“随时间推移的值”。举例来说,生日的流是一个 Subject,但年龄的流应该是一个 BehaviorSubject 。
在下面的示例中,BehaviorSubject 使用值0
进行初始化,当第一个观察者订阅时会得到0
。第二个观察者订阅时会得到值2
,尽管它是在值2
发送之后订阅的。
var subject = new Rx.BehaviorSubject(0); // 0是初始值
subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});
subject.next(1);
subject.next(2);
subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});
subject.next(3);
输出:
observerA: 0
observerA: 1
observerA: 2
observerB: 2
observerA: 3
observerB: 3
ReplaySubject
ReplaySubject
类似于 BehaviorSubject
,它可以发送旧值给新的订阅者,但它还可以_记录_ Observable 执行的一部分。
ReplaySubject
记录 Observable 执行中的多个值并将其回放给新的订阅者。
当创建 ReplaySubject
时,你可以指定回放多少个值:
var subject = new Rx.ReplaySubject(3); // 为新的订阅者缓冲3个值
subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});
subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);
subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});
subject.next(5);
With output:
observerA: 1
observerA: 2
observerA: 3
observerA: 4
observerB: 2
observerB: 3
observerB: 4
observerA: 5
observerB: 5
除了缓冲数量,你还可以指定 window time (以毫秒为单位)来确定多久之前的值可以记录。在下面的示例中,我们使用了较大的缓存数量100
,但 window time 参数只设置了500
毫秒。
var subject = new Rx.ReplaySubject(100, 500 /* windowTime */);
subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});
var i = 1;
setInterval(() => subject.next(i++), 200);
setTimeout(() => {
subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});
}, 1000);
从下面的输出可以看出,第二个观察者得到的值是3
、4
、5
,这三个值是订阅发生前的500
毫秒内发生的:
observerA: 1
observerA: 2
observerA: 3
observerA: 4
observerA: 5
observerB: 3
observerB: 4
observerB: 5
observerA: 6
observerB: 6
...
AsyncSubject
AsyncSubject 是另一个 Subject 变体,只有当 Observable 执行完成时(执行 complete()
),它才会将执行的最后一个值发送给观察者。
var subject = new Rx.AsyncSubject();
subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});
subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);
subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});
subject.next(5);
subject.complete();
输出:
observerA: 5
observerB: 5
AsyncSubject 和 last()
操作符类似,因为它也是等待 complete
通知,以发送一个单个值。
Operators (操作符)
尽管 RxJS 的根基是 Observable,但最有用的还是它的操作符。操作符是允许复杂的异步代码以声明式的方式进行轻松组合的基础代码单元。
什么是操作符?
操作符是 Observable 类型上的方法,比如 .map(...)
、.filter(...)
、.merge(...)
,等等。当操作符被调用时,它们不会改变已经存在的 Observable 实例。相反,它们返回一个新的 Observable ,它的 subscription 逻辑基于第一个 Observable 。
操作符本质上是一个纯函数 (pure function),它接收一个 Observable 作为输入,并生成一个新的 Observable 作为输出。订阅输出 Observable 同样会订阅输入 Observable 。
在下面的示例中,我们创建一个自定义操作符函数,它将从输入 Observable 接收的每个值都乘以10:
function multiplyByTen(input) {
var output = Rx.Observable.create(function subscribe(observer) {
input.subscribe({
next: (v) => observer.next(10 * v),
error: (err) => observer.error(err),
complete: () => observer.complete()
});
});
return output;
}
var input = Rx.Observable.from([1, 2, 3, 4]);
var output = multiplyByTen(input);
output.subscribe(x => console.log(x));
输出:
10
20
30
40
注意,订阅 output
会导致 input
Observable 也被订阅。我们称之为“操作符订阅链”。
实例操作符 vs. 静态操作符
什么是实例操作符? - 通常提到操作符时,我们指的是实例操作符,它是 Observable 实例上的方法。举例来说,如果上面的 multiplyByTen
是官方提供的实例操作符,它看起来大致是这个样子的:
Rx.Observable.prototype.multiplyByTen = function multiplyByTen() {
var input = this;
return Rx.Observable.create(function subscribe(observer) {
input.subscribe({
next: (v) => observer.next(10 * v),
error: (err) => observer.error(err),
complete: () => observer.complete()
});
});
}
实例运算符是使用 this
关键字来指代输入的 Observable 的函数。
注意,这里的 input
Observable 不再是一个函数参数,它现在是 this
对象。下面是我们如何使用这样的实例运算符:
var observable = Rx.Observable.from([1, 2, 3, 4]).multiplyByTen();
observable.subscribe(x => console.log(x));
什么是静态操作符? - 除了实例操作符,还有静态操作符,它们是直接附加到 Observable 类上的。静态操作符在内部不使用 this
关键字,而是完全依赖于它的参数。
静态操作符是附加到 Observable 类上的纯函数,通常用来从头开始创建 Observable 。
最常用的静态操作符类型是所谓的创建操作符。它们只接收非 Observable 参数,比如数字,然后创建一个新的 Observable ,而不是将一个输入 Observable 转换为输出 Observable 。
一个典型的静态操作符例子就是 interval
函数。它接收一个数字(非 Observable)作为参数,并生产一个 Observable 作为输出:
var observable = Rx.Observable.interval(1000 /* 毫秒数 */);
创建操作符的另一个例子就是 create
,已经在前面的示例中广泛使用。点击这里查看所有静态操作符列表。
然而,有些静态操作符可能不同于简单的创建。一些组合操作符可能是静态的,比如 merge
、combineLatest
、concat
,等等。这些作为静态运算符是有道理的,因为它们将多个 Observables 作为输入,而不仅仅是一个,例如:
var observable1 = Rx.Observable.interval(1000);
var observable2 = Rx.Observable.interval(400);
var merged = Rx.Observable.merge(observable1, observable2);
Marble diagrams (弹珠图)
要解释操作符是如何工作的,文字描述通常是不足以描述清楚的。许多操作符都是跟时间相关的,它们可能会以不同的方式延迟(delay)、取样(sample)、节流(throttle)或去抖动值(debounce)。图表通常是更适合的工具。弹珠图是操作符运行方式的视觉表示,其中包含输入 Obserable(s) (输入可能是多个 Observable )、操作符及其参数和输出 Observable 。
在弹珠图中,时间流向右边,图描述了在 Observable 执行中值(“弹珠”)是如何发出的。
在下图中可以看到解剖过的弹珠图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eT8birrM-1603866506607)(https://cn.rx.js.org/manual/asset/marble-diagram-anatomy.svg)]
在整个文档站中,我们广泛地使用弹珠图来解释操作符的工作方式。它们在其他环境中也可能非常有用,例如在白板上,甚至在我们的单元测试中(如 ASCII 图)。
Scheduler (调度器)
调度器 控制着何时启动 subscription 和何时发送通知。它由三部分组成:
- 调度器是一种数据结构。 它知道如何根据优先级或其他标准来存储任务和将任务进行排序。
- 调度器是执行上下文。 它表示在何时何地执行任务(举例来说,立即的,或另一种回调函数机制(比如 setTimeout 或 process.nextTick),或动画帧)。
- 调度器有一个(虚拟的)时钟。 调度器功能通过它的 getter 方法
now()
提供了“时间”的概念。在具体调度器上安排的任务将严格遵循该时钟所表示的时间。
调度器可以让你规定 Observable 在什么样的执行上下文中发送通知给它的观察者。
在下面的示例中,我们采用普通的 Observable ,它同步地发出值1
、2
、3
,并使用操作符 observeOn
来指定 async
调度器发送这些值。
var observable = Rx.Observable.create(function (observer) {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
})
.observeOn(Rx.Scheduler.async);
console.log('just before subscribe');
observable.subscribe({
next: x => console.log('got value ' + x),
error: err => console.error('something wrong occurred: ' + err),
complete: () => console.log('done'),
});
console.log('just after subscribe');
输出结果:
just before subscribe
just after subscribe
got value 1
got value 2
got value 3
done
注意通知 got value...
在 just after subscribe
之后才发送,这与我们到目前为止所见的默认行为是不一样的。这是因为 observeOn(Rx.Scheduler.async)
在 Observable.create
和最终的观察者之间引入了一个代理观察者。在下面的示例代码中,我们重命名了一些标识符,使得其中的区别变得更明显:
var observable = Rx.Observable.create(function (proxyObserver) {
proxyObserver.next(1);
proxyObserver.next(2);
proxyObserver.next(3);
proxyObserver.complete();
})
.observeOn(Rx.Scheduler.async);
var finalObserver = {
next: x => console.log('got value ' + x),
error: err => console.error('something wrong occurred: ' + err),
complete: () => console.log('done'),
};
console.log('just before subscribe');
observable.subscribe(finalObserver);
console.log('just after subscribe');
proxyObserver
是在 observeOn(Rx.Scheduler.async)
中创建的,它的 next(val)
函数大概是下面这样子的:
var proxyObserver = {
next: (val) => {
Rx.Scheduler.async.schedule(
(x) => finalObserver.next(x),
0 /* 延迟时间 */,
val /* 会作为上面函数所使用的 x */
);
},
// ...
}
async
调度器操作符使用了 setTimeout
或 setInterval
,即使给定的延迟时间
为0。照例,在 JavaScript 中,我们已知的是 setTimeout(fn, 0)
会在下一次事件循环迭代的最开始运行 fn
。这也解释了为什么发送给 finalObserver
的 got value 1
发生在 just after subscribe
之后。
调度器的 schedule()
方法接收一个 delay
参数,它指的是相对于调度器内部时钟的一段时间。调度器的时钟不需要与实际的挂钟时间有任何关系。这也就是为什么像 delay
这样的时间操作符不是在实际时间上操作的,而是取决于调度器的时钟时间。这在测试中极其有用,可以使用虚拟时间调度器来伪造挂钟时间,同时实际上是在同步执行计划任务。
调度器类型
async
调度器是 RxJS 提供的内置调度器中的一个。可以通过使用 Scheduler
对象的静态属性创建并返回其中的每种类型的调度器。
调度器 | 目的 |
---|---|
null | 不传递任何调度器的话,会以同步递归的方式发送通知。用于定时操作或尾递归操作。 |
Rx.Scheduler.queue | 当前事件帧中的队列调度(蹦床调度器)。用于迭代操作。 |
Rx.Scheduler.asap | 微任务的队列调度,它使用可用的最快速的传输机制,比如 Node.js 的 process.nextTick() 或 Web Worker 的 MessageChannel 或 setTimeout 或其他。用于异步转换。 |
Rx.Scheduler.async | 使用 setInterval 的调度。用于基于时间的操作符。 |
使用调度器
你可能在你的 RxJS 代码中已经使用过调度器了,只是没有明确地指明要使用的调度器的类型。这是因为所有的 Observable 操作符处理并发性都有可选的调度器。如果没有提供调度器的话,RxJS 会通过使用最小并发原则选择一个默认调度器。这意味着引入满足操作符需要的最小并发量的调度器会被选择。例如,对于返回有限和少量消息的 observable 的操作符,RxJS 不使用调度器,即 null
或 undefined
。对于返回潜在大量的或无限数量的消息的操作符,使用 queue
调度器。对于使用定时器的操作符,使用 aysnc
调度器。
因为 RxJS 使用最少的并发调度器,如果出于性能考虑,你想要引入并发,那么可以选择不同的调度器。要指定具体的调度器,可以使用那些采用调度器的操作符方法,例如 from([10, 20, 30], Rx.Scheduler.async)
。
静态创建操作符通常可以接收调度器作为参数。 举例来说,from(array, scheduler)
可以让你指定调度器,当发送从 array
转换的每个通知的时候使用。调度器通常作为操作符的最后一个参数。下面的静态创建操作符接收调度器参数:
bindCallback
bindNodeCallback
combineLatest
concat
empty
from
fromPromise
interval
merge
of
range
throw
timer
使用 subscribeOn
来调度 subscribe()
调用在什么样的上下文中执行。 默认情况下,Observable 的 subscribe()
调用会立即同步地执行。然而,你可能会延迟或安排在给定的调度器上执行实际的 subscription ,使用实例操作符 subscribeOn(scheduler)
,其中 scheduler
是你提供的参数。
使用 observeOn
来调度发送通知的的上下文。 正如我们在上面的示例中所看到的,实例操作符 observeOn(scheduler)
在源 Observable 和目标观察者之间引入了一个中介观察者,中介负责调度,它使用给定的 scheduler
来调用目标观察者。
实例操作符可能会接收调度器作为参数。
像 bufferTime
、debounceTime
、delay
、auditTime
、sampleTime
、throttleTime
、timeInterval
、timeout
、timeoutWith
、windowTime
这样时间相关的操作符全部接收调度器作为最后的参数,并且默认的操作是在 Rx.Scheduler.async
调度器上。
其他接收调度器作为参数的实例操作符:cache
、combineLatest
、concat
、expand
、merge
、publishReplay
、startWith
。
注意,cache
和 publishReplay
都接收调度器是因为它们使用了 ReplaySubject 。ReplaySubjects 的构造函数接收一个可选的调度器作为最后的参数,因为 ReplaySubject 可能会处理时间,这只在调度器的上下文中才有意义。默认情况下,ReplaySubject 使用 queue
调度器来提供时钟。
操作符分类
RxJS提供了各种API来创建数据流:
- 单值:of, empty, never
- 多值:from
- 定时:interval, timer
- 从事件创建:fromEvent
- 从Promise创建:fromPromise
- 自定义创建:create
创建出来的数据流是一种可观察的序列,可以被订阅,也可以被用来做一些转换操作,比如:
- 改变数据形态:map, mapTo, pluck
- 过滤一些值:filter, skip, first, last, take
- 时间轴上的操作:delay, timeout, throttle, debounce, audit, bufferTime
- 累加:reduce, scan
- 异常处理:throw, catch, retry, finally
- 条件执行:takeUntil, delayWhen, retryWhen, subscribeOn, ObserveOn
- 转接:switch
也可以对若干个数据流进行组合:
- concat,保持原来的序列顺序连接两个数据流
- merge,合并序列
- race,预设条件为其中一个数据流完成
- forkJoin,预设条件为所有数据流都完成
- zip,取各来源数据流最后一个值合并为对象
- combineLatest,取各来源数据流最后一个值合并为数组
操作符有着不同的用途,它们可作如下分类:创建、转换、过滤、组合、错误处理、工具,等等。在下面的列表中,你可以按分类组织好的所有操作符。
创建操作符
create
:自定义创建Observable
const observable = Observable.create((observe) => {
observe.next('value')
})
observable.subscribe({
next:() => {
},
complete: () => {
},
error: () => {
}
}
of
:单一值转为流
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PLGcl3QV-1603866506609)(https://cn.rx.js.org/img/of.png)]
它接收任意多个参数,参数可以是任意类型,然后它会把这些参数逐个放入流中。
var source = of('Jerry', 'Anna');
source.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log(error)
}
});
from
:类数组转为流
从一个数组、类数组对象、Promise、迭代器对象或者类 Observable 对象创建一个 Observable.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-69A0I98t-1603866506617)(https://cn.rx.js.org/img/from.png)]
1、将数组转化为 Observable
var array = [10, 20, 30];
var result = Rx.Observable.from(array);
result.subscribe(x => console.log(x));
// 结果如下:
// 10 20 30
2、将一个无限的迭代器(来自于 generator)转化为 Observable。
function* generateDoubles(seed) {
var i = seed;
while (true) {
yield i;
i = 2 * i; // double it
}
}
var iterator = generateDoubles(3);
var result = Rx.Observable.from(iterator).take(10);
result.subscribe(x => console.log(x));
// Results in the following:
// 3 6 12 24 48 96 192 384 768 1536
3、将Observable转化为Observable:
from([of(1,2,3),of('a','b','c')]).pipe(
concatAll(),
).subscribe(res => {
console.log(res);
});
// 依次输出:1,2,3,a,b,c
4、将字符串转化为Observable
from('test').subscribe(res => {
console.log(res);
})
// 依次输出test
5、从promise转化为Observable
from(new Promise(resolve => resolve('hello'))).subscribe(res => {
console.log(res);
})
6、从集合中转化为Observable
const m = new Map();
m.set('a', 'hi');
m.set('b', 'tony');
from(m).subscribe(res => {
console.log(res);
})
// 依次输出:["a","hi"],["b","tony"]
fromPromise
:Promise 转为流
接受一个 Promise,当这个 Promise 有了输出时,就把这个输出放入流中。
要注意的是,当 Promise 作为参数传给 fromPromise
时,这个 Promise 就开始执行了,你没有机会防止它被执行。
如果你需要这个 Promise 被消费时才执行,那就要改用 defer
创建器。
如果 Promise resolves 一个值, 输出 Observable 发出这个值然后完成。 如果 Promise 被 rejected, 输出 Observable 会发出相应的错误。
// 将 Fetch 返回的 Promise 转化为 Observable。
var result = Rx.Observable.fromPromise(fetch('http://myserver.com/'));
result.subscribe(x => console.log(x), e => console.error(e));
fromEvent
:创建一个Observable,该Observable发出来自给定事件对象的指定类型事件。
// 发出 DOM document 上的点击事件。
var clicks = Rx.Observable.fromEvent(document, 'click');
clicks.subscribe(x => console.log(x));
// 结果:
// 每次点击 document 时,都会在控制台上输出 MouseEvent 。
range
: 创建一个Observable ,它发出指定范围内的数字序列。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W7fAqkgw-1603866506619)(https://cn.rx.js.org/img/range.png)]
它接受两个数字型参数,一个起点,一个终点,然后按 1 递增,把中间的每个数字(含边界值)放入流中。
// 发出从1到10的数
var numbers = Rx.Observable.range(1, 10);
numbers.subscribe(x => console.log(x));
defer
:惰性创建流,当且仅当它被订阅的时候才创建。
它的参数是一个用来生产流的工厂函数。也就是说,当消费方需要流(注意不是需要流中的值)的时候,就会调用这个函数,创建一个流,并从这个流中进行消费(取数据)。
因此,当我们定义 defer 的时候,实际上还不存在一个真正的流,只是给出了创建这个流的方法,所以叫惰性创建流。
// 随机订阅点击或者 interval Observable
var clicksOrInterval = Rx.Observable.defer(function () {
if (Math.random() > 0.5) {
return Rx.Observable.fromEvent(document, 'click');
} else {
return Rx.Observable.interval(1000);
}
});
clicksOrInterval.subscribe(x => console.log(x));
// 结果如下:
// 如果Math.random()返回的值大于0.5,它会监听"document"上的点击事件; 当document
// 被点击,它会将点击事件对象打印到控制台。 如果结果小于0.5它会每秒发出一个从0开始自增数。
timer
:创建一个 Observable,该 Observable 在初始延时(initialDelay)之后开始发送并且在每个时间周期( period)后发出自增的数字。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eNsmosnG-1603866506621)(https://cn.rx.js.org/img/timer.png)]
它有两个数字型的参数,第一个是首次等待时间,第二个是重复间隔时间。从图上可以看出,它实际上是个无尽流 —— 没有终止线。因此它会按照预定的规则往流中不断重复发出数据。如果第二个参数为空则发送第一个参数后,终止,执行complete函数。
要注意,虽然名字有相关性,但它不是 setTimeout
的等价物,事实上它的行为更像是 setInterval
。
// 每隔1秒发出自增的数字,3秒后开始发送。
var numbers = Rx.Observable.timer(3000, 1000);
numbers.subscribe(x => console.log(x));
interval
:以指定时间间隔发出连续的数字
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yCsn82Ep-1603866506621)(https://cn.rx.js.org/img/interval.png)]
它和 timer 唯一的差别是它只接受一个参数。事实上,它就是一个语法糖,相当于 timer(1000, 1000)
,也就是说初始等待时间和间隔时间是一样的。
// 每1秒发出一个自增数
var numbers = Rx.Observable.interval(1000);
numbers.subscribe(x => console.log(x));
如果需求确实是 interval
的语义,那么就优先使用这个语法糖,毕竟,从行为上它和 setInterval
几乎是一样的。
其余创建操作符
ajax
bindCallback
bindNodeCallback
empty
:创建一个空的Observable,如果订阅这个Observable,它会立即响应complete函数never
:一个一直存在却什么都不做的observablefromEventPattern
generate
repeat
: 成功时重试。repeat 很少会单独用,一般会组合上 delay 操作,以提供暂停时间,否则就容易 DoS 了服务器。repeatWhen
throw
:拋出错误
转换操作符
-
mapTo
:将参数转换为一个固定值var source = interval(1000); var newest = source.pipe(mapTo(2)); newest.subscribe(console.log); // 2 // 2 // 2 // 2..
-
scan
: 数据累加计算var main = from('hello').pipe( zip(interval(500), (x, y) => x) ) const example = main.pipe( scan( (origin,next)=> origin + next ) ) example.subscribe({ next: value => { console.log(value); }, error: err => { console.log("Error: " + err); }, complete: () => { console.log("complete"); } }); // h // he // hel // hell // hello // complete
过滤操作符
-
debounceTime
:debounce 在每次收到元素,他会先把元素 cache 住并等待一段时间,如果这段时间内已经没有收到任何元素,则把元素送出;如果这段时间内又收到新的元素,则会把原本的cache住的元素释放掉并重新计时,不断反复。var example = interval(300).pipe(take(5),debounceTime(1000)); example.subscribe({ next: (value) => { console.log(value); }, error: (err) => { console.log('Error: ' + err); }, complete: () => { console.log('complete'); } }); // 4 // complete
-
distinct
:distinct会和已经拿到的数据比较过滤掉重复的元素。var example = from(['a', 'b', 'c', 'a', 'b']).pipe( zip(interval(300), (x, y) => x), distinct() ) example.subscribe({ next: (value) => { console.log(value); }, error: (err) => { console.log('Error: ' + err); }, complete: () => { console.log('complete'); } }); // a // b // c // complete
distinct第一个参数是个函数,函数返回值就是distinct比较的值:
var source = from([{ value: 'a'}, { value: 'b' }, { value: 'c' }, { value: 'a' }, { value: 'c' }]).pipe( zip(Rx.Observable.interval(300), (x, y) => x) ) . var example = source.pipe( distinct((x) => { return x.value }) ) example.subscribe({ next: (value) => { console.log(value); }, error: (err) => { console.log('Error: ' + err); }, complete: () => { console.log('complete'); } }); // {value: "a"} // {value: "b"} // {value: "c"} // complete
但是distinct底层是创建一个set来辅助去重,如果数据很大,可能导致set过大,这个时候就需要设置distinct第二个参数来刷新set,第二个 参数是个observable到发起订阅的时候就会清空set
var flushes = interval(1300); var example = from(['a', 'b', 'c', 'a', 'c']).pipe( zip(interval(300), (x, y) => x), distinct( null,flushes ) ) example.subscribe({ next: (value) => { console.log(value); }, error: (err) => { console.log('Error: ' + err); }, complete: () => { console.log('complete'); } }); // a // b // c // c // complete
-
distinctUntilChanged
:distinctUntilChanged与distinct不同之处就是,distinctUntilChanged只会比较相邻两次输入var example = from(['a', 'b', 'c', 'c', 'b']).pipe( .zip(interval(300), (x, y) => x), distinctUntilChanged() ) example.subscribe({ next: (value) => { console.log(value); }, error: (err) => { console.log('Error: ' + err); }, complete: () => { console.log('complete'); } }); // a // b // c // b // complete
-
first
:取第一个数然后结束,和take(1)效果一样 -
var source = interval(1000); var example = source.pipe(take(3)); example.subscribe({ next: (value) => { console.log(value); }, error: (err) => { console.log('Error: ' + err); }, complete: () => { console.log('complete'); } });
-
throttle
:跟 debounce 的不同是 throttle 會先開放送出元素,等到有元素被送出就會沈默一段時間,等到時間過了又會继续发送元素,防止某个事件频繁触发,影响效率。var example = interval(300).pipe(take(5), throttleTime(1000) ); example.subscribe({ next: (value) => { console.log(value); }, error: (err) => { console.log('Error: ' + err); }, complete: () => { console.log('complete'); } }); // 0 // 4 // complete
组合操作符
-
concat
:concat 将多个observable串接起来前一个完成好了,再执行下一个。const source1 = interval(1000).pipe(take(3)); const source2 = of(3); const source3 = of (4,5); const example = source1.pipe(concat(source2,source3)) example.subscribe({ next: value => { console.log(value); }, error: err => { console.log("Error: " + err); }, complete: () => { console.log("complete"); } }); // 0 // 1 // 2 // 3 // 4 // 5 // complete
-
merge
:merge使用方式和concat一样,区别就是merge处理的Observable是异步执行的,没有先后顺序。const source1 = interval(1000).pipe(take(3)); const source2 = of(3); const source3 = of (4,5); const example = source1.pipe(merge(source2,source3)) example.subscribe({ next: value => { console.log(value); }, error: err => { console.log("Error: " + err); }, complete: () => { console.log("complete"); } }); // 3 // 4 // 5 // 0 // 1 // 2 // complete
-
startWith
:塞一个初始值给Observable -
const source1 = interval(1000).pipe(take(3)); const source2 = interval(3000).pipe(takee(3)); const example = source1.pipe(zip(source2, (x, y) => { return x + y })); example.subscribe({ next: value => { console.log(value); }, error: err => { console.log("Error: " + err); }, complete: () => { console.log("complete"); } }); // 0 // 2 // 4 // complete
多播操作符
错误处理操作符
工具操作符
-
delay
: delay会将observable第一次发出订阅的时间延迟const example = interval(300).pipe(take(5),delay(500)); example.subscribe({ next: (value) => { console.log(value); }, error: (err) => { console.log('Error: ' + err); }, complete: () => { console.log('complete'); } }); // 0 // 1 // 2 // 3 // 4
-
finally
-
let
条件和布尔操作符
数学和聚合操作符
总结
RxJS 中的很多操作符都遵循着同样的命名模式。比如:
xxxWhen - 满足条件时 xxx
它接受一个 Observable 型参数作为条件流,一旦这个条件流中出现任意数据,则进行 xxx 操作。
如 retryWhen(notifier$)
,其中的 notifier$
就是一个条件流。当输入流出现异常时,就会开始等待 notifier$
流中出现数据,一旦出现了任何数据(不管是什么值),就会开始执行重试逻辑。
xxxCount - 拿到 n 个数据项时 xxx
它接受一个数字型参数作为阈值,一旦从输入流中取到了 n 个数据,则进行 xxx 操作。
如 bufferCount(3)
表示每拿到 3 个数据就进行一次 buffer
操作。
这个操作可以看做是 xxxWhen
的语法糖。
xxxTime - 超时后 xxx
它接受一个超时时间作为参数,从输入流中取数据,一旦到达超时时间,则执行 xxx 操作。
比如前面讲过的 debounceTime
其实遵循的就是这种模式。
这个操作可以看做 xxxWhen
的语法糖。
xxxTo - 用立即量代替 Lambda 表达式
它接受一个立即量作为参数,相当于 xxx(()=> value))
。
比如 mapTo('a')
其实是 map(()=>'a')
的语法糖,也就是说无论输入流中给出的值是什么,我往输出流中放入的都是这个固定的值。