前言
这段时间在项目中接触到了 rxjs, 一开始接触 rxjs 时,是非常抗拒的,心想这是啥玩意,咋这么难读懂。渐渐了解 rxjs,发现这玩意是真的好用呀,因此通过写文章的形式巩固自己对 rxjs 的理解。
是什么?
RxJS
是一个基于可观测数据流 Stream
结合观察者模式和迭代器模式的一种异步编程的应用库。RxJS
是 Reactive
Extensions
在 JavaScript
上的实现。
基本概念:
- Observable(可观察对象): 表示一个可调用的未来值或事件的集合。
- Observer(观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。
- Subscription(订阅): 表示Observable的执行,它主要用于取消 Observable 的执行。
- Operator(操作符): 采用函数式编程风格的纯函数,使用像 map、filter、concat 等这样的操作符来处理集合。
- Subject(主体): 相当于EventEmitter,并且是将值或事件多路推送给多个Observer。
- Schdulers(调度器): :用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeout 或 requestAnimationFrame。
怎么用?
Observable
Observable 是由多个值组成的延迟推送集合。
创建 Observable
可以通过构造函数创建 Observable, 该构造函数接受一个参数:subscriber
。
import { Observable } from "rxjs";
const observable = new Observable((subscriber) => {
setInterval(() => {
subscriber.next(1);
}, 1000);
});
observable.subscribe((val) => {
console.log(`observable, `, val);
});
从上面例子我们看出,可以通过构造函数创建 Observable,每秒会发出值 1。
import { Observable } from "rxjs";
const observable = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.next(4);
setTimeout(() => {
subscriber.next(5);
});
});
console.log("start");
observable.subscribe((val) => {
console.log(`observable, `, val);
});
console.log("end");
从输出结果来看, Observable 能同步或异步地传递值
Observer
Observer 是 Observable 传递值的消费者。观察者只是一组回调,对应于 Observable 传递的每种类型的通知:next
、error
和complete
。
// 形式如下
const 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(observer);
从👆上面例子来看, 观察者是具有三个回调的对象,用于 Observable 可能传递的每种类型的通知。
我们在调用subscribe
的时候可以使用这两种方式,以一个对象形式,该对象具备next
、error
、complete
三个方法(都是可选的),或者直接传入函数的方式,参数前后分别为next
、error
、complete
。
如果发送了错误 error 或完成通知 complete,则之后无法发送任何其他通知。
Subject
Subject 和 Observable 类似,你可以订阅 Subject,它会像订阅 Observable 一样。它也有类似 next()、error() 以及 complete() 的方法,就像你平时传给 Observable 构造函数的 Observer 一样。
但 Subject 主要是为了多播,而 Observable 默认是单播的,即对于每个订阅者,都只有一个独立的 Observable 与之对应。
import { Observer, Observable, Subject } from "rxjs";
const observable = Observable.create(
(observer: { next: (arg0: number) => void }) => {
observer.next(Math.random());
}
);
// 订阅者1
observable.subscribe((data: any) => {
// 每当调用一次 observable.subscribe 就会有一个新的 Observable 产生, 即俩次的data是不一样的
// 因为 Observable 在设计上就是单播
console.log(`data1`, data);
});
// 订阅者2
observable.subscribe((data: any) => {
console.log(`data2`, data);
});
执行结果:
通过结果我们可以知道,每当我们调用一次 Observable.subscribe() 时,一个新的 Observable execution 就会被启动。 因此,如果你希望使多个订阅者收到相同的数据,那么使用 Subject 是一个不错的选择。
当我们订阅 Subject 的时候,并不会像 Observable 一样,创建多个发布者。
import { Observer, Observable, Subject } from "rxjs";
// Subject 的设计上是多播,当我们订阅Subject时,他只会在现有的观察者列表中多注册一个新的观察者
const subject = new Subject();
// subject.next(400); // 写在这儿不会触发
subject.subscribe((data) => {
// data3/data4的值是一样的
console.log(`data3`, data);
});
subject.subscribe((data) => {
console.log(`data4`, data);
});
subject.next(Math.random());
执行结果:
通过结果我们可以发现,当我们订阅 Subject 时,并不会像 Observable 那样产生多个发布者。
当然,这并不是 Subject 的唯一用途,例如它还可以将一个单播 Observable 转化为多播。
import { Observer, Observable, Subject } from "rxjs";
const observable = Observable.create(
(observer: { next: (arg0: number) => void }) => {
observer.next(Math.random());
}
);
// Subject 的设计上是多播,当我们订阅Subject时,他只会在现有的观察者列表中多注册一个新的观察者
const subject = new Subject();
// observable.subscribe(subject); 👉 放在这儿不会触发 subscribe
subject.subscribe((data) => {
// data3/data4的值是一样的
console.log(`data3`, data);
});
subject.subscribe((data) => {
console.log(`data4`, data);
});
// 借助 Subject 将 Observable 转化为多播
observable.subscribe(subject);
我们将 Observable 的订阅者改成 Subject, 由 Subject 将事件传递给订阅者,从而将 Observable 转化为单播。
BehaviorSubject
BehaviorSubject 是 Subject 的一种形式,其特点是会存储当前的值,每当有订阅事件时,那么 BehaviorSubject 将给订阅者发送当前存储的值。
import { BehaviorSubject } from "rxjs";
const subject = new BehaviorSubject(50);
subject.subscribe((data) => {
console.log("Subject A", data);
});
subject.next(100); // BehaviorSubject
subject.subscribe((data) => {
// 只要订阅了BehaviorSubject, BehaviorSubject会直接返回给订阅者当前存储的值
console.log("Subject B", data);
});
subject.next(200);
console.log(subject.value); // 获取上一次的广播的值
输出:
订阅了BehaviorSubject, BehaviorSubject会直接返回给订阅者当前存储的值
ReplaySubject
ReplaySubject 相对于 BehaviorSubject 而言,ReplaySubject 可以发送旧数据,并可以指定存储的数量和过期时间。
import { ReplaySubject } from "rxjs";
const replaySubject = new ReplaySubject(2); // 指定只存储2个旧的值
// 订阅者A
replaySubject.subscribe((data) => {
console.log(`Subscriber A:`, data);
});
replaySubject.next(1);
replaySubject.next(2);
replaySubject.next(3);
// 订阅者B
replaySubject.subscribe((data) => {
// 如果之前有存储的值,会触发订阅
console.log(`Subscriber B:`, data);
});
replaySubject.next(4);
输出:
订阅了ReplaySubject,ReplaySubject会指直接将存储的值返回给订阅者
AsyncSubject
AsyncSubject 只会在 Observable 完成后,将其最终值发给订阅者
import { AsyncSubject } from "rxjs";
const asyncSubject = new AsyncSubject();
// 订阅者A
asyncSubject.subscribe((data) => {
console.log(`Subscriber A:`, data);
});
asyncSubject.next(1);
asyncSubject.next(2);
asyncSubject.next(3);
// 订阅者B
asyncSubject.subscribe((data) => {
console.log(`Subscriber B:`, data);
});
asyncSubject.next(4);
asyncSubject.complete();
常见的操作符
操作符本质上是一个纯函数,它将一个 Observable 作为输入并生成另一个 Observable 作为输出。这是一个纯粹的操作:之前的 Observable 保持不变。
Piping(管道)
管道运算符是一个函数,它类似于 Gulp 中的 pipe,可以将功能进行流水线式的组合。例如:
observable.pipe(fn1, fn2, fn3, fn4);
创建运算符
创建运算符是可以用来创建 Observable 的函数。
interval
interval 创建的 Observable
可以在指定时间内发出连续的数字,其实就跟我们使用setInterval
这种模式差不多。在我们需要获取一段连续的数字时,或者需要定时做一些操作时都可以使用该操作符实现我们的需求
import { interval } from "rxjs";
const observable = interval(1000 /* number of milliseconds */);
observable.subscribe((data) => {
console.log(`interval: `, data); // 从 0 开始,每一秒会输出一个数字
});
of
of 会创建一个 Observable, 输出的值取决于传入的参数。类似于 interval,它和 pipe 很相似,有流水线的味道。
const observableOf = of(1, 2, 3);
observableOf.subscribe((data) => {
console.log(`observableOf`, data); // 依次输出 1、2、3
});
from
from 可以将数组、类数组、Promise、迭代器对象或者类 Observable 对象转化并产生新的 Observable。
const observableFrom = from([1, 2, 3]);
observableFrom.subscribe((data) => {
console.log(`observableFrom: `, data); // 依次输出1、2、3
});
const observableFromPromise = from(
new Promise((resolve) => setTimeout(() => resolve(1), 3000))
);
observableFromPromise.subscribe((data) => {
console.log(`observableFromPromise`, data); // 3s 后输出1
});
fromEvent
创建一个 Observable,该 Observable
发出来自给定事件对象的指定类型事件。可用于浏览器环境中的Dom
事件或Node
环境中的EventEmitter
事件等。
const observableFromEvent = fromEvent(document, "click");
observableFromEvent.subscribe((data) => {
console.log(`observableFromEvent: `, data);
});
rang
创建一个新的 Observable 并指定范围内的数字序列。
const observableRange = range(1, 4);
observableRange.subscribe((data) => {
console.log(`observableRange: `, data);
});
empty
该操作符创建一个什么数据都不发出,直接发出完成通知的操作符。
repeat
创建一个新的 Observable 重复源上发出的所有值。就像 retry
,但是用于非错误情况
import { of } from "rxjs";
import { repeat } from "rxjs/operators";
const observableRepeat = of(1, 2, 3).pipe(repeat(3));
observableRepeat.subscribe((data) => {
console.log(`observableRepeat`, data); // 依次输出 1、2、3
});
转化运算符
buffer
收集过往的数据存放到数组中,仅当 buffer 传入的 Observable 发出通知时,才会发出此数组。这相当于一个缓冲区,将数据收集起来后,等到一个信号来临在释放出去
import { interval, fromEvent } from "rxjs";
import { buffer } from "rxjs/operators";
const myInterval = interval(1000);
const bufferBy = fromEvent(document, "click");
const myBufferedInterval = myInterval.pipe(buffer(bufferBy));
const subject = myBufferedInterval.subscribe((data: any) => {
console.log(`Buffered values: `, data);
});
结果:
concatMap
concatMap 接收一个函数,该函数将值映射成内部 Observable,并按顺序依次订阅和发出。
// 发出延迟值
const sourceConcatMap = of(2000, 1000);
// 将内部 observable 映射成 source,当前一个完成时发出结果后订阅下一个
const resultConcatMap = sourceConcatMap.pipe(
concatMap((val) => of(`Delayed by: ${val}ms`).pipe(delay(val)))
);
// 输出: With concatMap: Delayed by: 2000ms, With concatMap: Delayed by: 1000ms
resultConcatMap.subscribe((val) => console.log(`With concatMap: ${val}`));
结果:
mergeMap
mergeMap 和 concatMap 类似,接受一个函数,该函数将值映射成内部 Observable。但和 concatMap 有区别,mergeMap 并不会等上一次的订阅完成后,在进行下一次的订阅。
const source = of(2000, 1000);
const exampleConcatMap1 = source.pipe(
concatMap((val) => of(`Delayed by: ${val}ms`).pipe(delay(val)))
);
exampleConcatMap1.subscribe((val) => {
console.log(`With concatMap: ${val}`);
});
const mergeMapExample = source.pipe(
delay(5000), // 确保比 concatMap 晚执行
mergeMap((val) => of(`Delayed by: ${val}ms`).pipe(delay(val)))
);
mergeMapExample.subscribe((val) => {
console.log(`With mergeMap: ${val}`);
});
map
map 接受一个函数,该函数可以将值进行转化,返回值即可作为订阅者实际获取到的值。
const sourceMap = interval(1000).pipe(take(3));
const resultMap = sourceMap.pipe(map((x) => x * 2));
resultMap.subscribe((x) => {
console.log(`resultMap: `, x); // 每隔 1 s 依次输出 0/2/4
});
mapTo
mapTo 忽略数据源发送的数据,将每个发出值映射成常量
const sourceMapTo = interval(1000).pipe(take(3));
const resultMapTo = sourceMapTo.pipe(mapTo("Hello world"));
resultMapTo.subscribe((data) => {
console.log(`resultMapTo: `, data);
});
switchMap
switchMap 主要作用首先会对多个 Observable 进行合并,并且具备打断能力,也就是说合并的这个几个Observable,某个 Observable 最先开始发送数据,这个时候订阅者能正常的接收到它的数据,但是这个时候另一个 Observable 也开始发送数据了,那么第一个 Observable 发送数据就被打断了,只会发送后来者发送的数据。
const btn = document.createElement("button");
btn.innerText = "try try";
document.body.append(btn);
const sourceSwithMap = fromEvent(btn, "click");
const resultSwithMap = sourceSwithMap.pipe(
switchMap(() => interval(1000).pipe(take(3)))
);
resultSwithMap.subscribe((data) => {
console.log(`resultSwithMap `, data);
});
scan
scan 是累加器操作符,作用类似于 reduce 函数
const sourceScan = of(1, 2, 3);
const exampleScan = sourceScan.pipe(scan((acc, curr) => acc + curr, 0));
exampleScan.subscribe((val) => {
console.log(`exampleScan: `, val); // 依次输出 1、3、6
});
过滤操作符
take
只发出源 Observable 最初发出的的 N 个值。
import { interval } from "rxjs";
import { take } from "rxjs/operators";
interval(1000)
.pipe(take(3))
.subscribe((data) => {
console.log(`data`, data);
});
skip
skip 返回一个 Observable, 会跳过前面的 N 个值。
from([1, 2, 3, 4, 5])
.pipe(skip(2))
.subscribe((data) => {
console.log(`skip`, data);
});
filter
对 Observable 发出的值进行过滤,类似于数组的 filter。
from([1, 2, 3, 4, 5])
.pipe(filter((x) => x > 3))
.subscribe((data) => {
console.log(`filter`, data);
});
结果:
distinct
类似于 Set 的作用,过滤重复的值
from([1, 2, 2, 3, 3, 4])
.pipe(distinct())
.subscribe((val) => {
console.log("distinct", val);
});
结果:
debounceTime
debounceTime 和防抖类似,在规定的时间内 Observable 持续发出,只会接受最后一个值
interval(1000)
.pipe(take(3), debounceTime(2000))
.subscribe((data) => {
console.log(`debounceTime`, data); // 第 5 秒的时候发出值 2
});
throttleTime
throttleTime 和节流类似,在规定的时间内只会发出一个值。
interval(1000)
.pipe(take(3), throttleTime(2000))
.subscribe((data) => {
console.log(`throttleTime`, data); // 依次输出 0 和 2
});
组合操作符
concatAll
concatAll 会顺序将依次合并合并的 Observable, 将高阶的 Observable 扁平化。
const source = interval(2000);
const example = source.pipe(
map((val) => of(val + 10)),
concatAll() // 合并内部的 observable 的值
);
example.subscribe((val) => {
console.log(`val: `, val);
});
结果:
如何去除了 concatAll 将返回一个 Observable。
const source = interval(2000);
const example = source.pipe(
map((val) => of(val + 10))
// concatAll() // 合并内部的 observable 的值
);
example.subscribe((val) => {
console.log(`val: `, val);
});
结果:
zip
zip 会将多个 Observable 组合以创建一个 Observable,返回值类似于 Promise.all, 值是由所有输入 Observables 的值按顺序计算而来的。
const s1 = interval(1000).pipe(take(3));
const s2 = interval(1000).pipe(take(5));
const result = zip(s1, s2);
result.subscribe((data) => {
console.log(`data: `, data);
});
只要有一个 Observable 结束, 则 zip 就结束。
startWidth
startWidth 会往 Observable 插入第一个值。
const sourceStartWith = interval(1000).pipe(take(3));
const resStartWith = sourceStartWith.pipe(startWith(666));
resStartWith.subscribe((data) => {
console.log(`data => `, data);
});
多播操作符
multicast
使用 multicast 操作符返回一个 Subject,可以将一个单播的 Observable 转化为多播的形式。
const source = interval(500).pipe(take(5));
const multicasted = source.pipe(
multicast(new Subject())
) as ConnectableObservable<number>;
const subscriptionA = multicasted.subscribe((v) => console.log("A: " + v));
const subscriptionB = multicasted.subscribe((v) => console.log("B: " + v));
const subscriptionConnect = multicasted.connect();
setTimeout(() => {
subscriptionA.unsubscribe();
subscriptionB.unsubscribe();
// subscriptionB退订后,source已经没有订阅者了,要加上这句才是真正的退订
subscriptionConnect.unsubscribe();
}, 3000);
refCount
如果觉的 multicast必须调用 connect方法才能推送值,还要 multicasted.unsubscribe()才能真正结束推送有些麻烦,就可以用 refCount。
refCount:当有 Observer订阅源 Observable时,自动调用 connect,当 Observer全部 unsubscribe后,即没有 Observer了,自动调用 connect().unsubscribe()退订。
const source = interval(500);
const refCounted = source.pipe(
multicast(new Subject()),
refCount()
);
const subscriptionA = refCounted.subscribe(v => console.log('A: ' + v));
const subscriptionB = refCounted.subscribe(v => console.log('B: ' + v));
setTimeout(() => {
subscriptionA.unsubscribe();
subscriptionB.unsubscribe();
}, 3000);
publish
multicast(new Subject())
这段代码很常用,可用 publish
将其简化
const refCounted = source.pipe(
multicast(new Subject()),
refCount()
); // 等价于 👇
const refCounted = source.pipe(publish(), refCount());
share
publish(), refCount()
可以通过 share 将其简化
const refCounted = source.pipe(publish(), refCount()); // 等价于 👇
const shared = source.pipe(share());
错误处理操作符
# catch / catchError
捕获 rxjs 错误
import { of, throwError } from "rxjs";
import { catchError } from "rxjs/operators";
const sourceError = throwError("This is an error!");
const exampleError = sourceError.pipe(
catchError((val) => of(`I caught: ${val}`))
);
exampleError.subscribe((val) => {
console.log(`exampleError`, val);
});
retry
如果发生错误,以指定次数重试 observable 序列
const sourceRetry = interval(1000);
const exampleRetry = sourceRetry.pipe(
mergeMap((val) => {
if (val > 5) {
return throwError("Error!");
}
return of(val);
}),
retry(2)
);
exampleRetry.subscribe({
next: (val) => console.log(val),
error: (val) => console.log(`get Error: ${val}`),
});
retryWhen
当发生错误时,基于自定义的标准来重试 observable 序列
const source = interval(1000);
const example = source.pipe(
map((val) => {
if (val > 5) {
throw val;
}
return val;
}),
retryWhen((errors) =>
errors.pipe(
tap((val) => console.log(`Error: ${val}`)),
// 5秒后重启
delayWhen((val) => timer(val * 1000))
)
)
);
example.subscribe((val) => console.log(val));
工具操作符
toPromise
将 Observable 转化为 Promise
import { of } from "rxjs";
of(5)
.toPromise()
.then((data) => {
console.log(`toPromise`, data); // 输出5
});
toArray
将 Observable 的值转化为数组
interval(500)
.pipe(take(5), toArray())
.subscribe((data) => {
console.log(`toArray`, data); // 👉 2.5s 后输出 [ 0, 1, 2, 3, 4 ]
});
delay
delay 延迟 n 秒输出
of(2)
.pipe(delay(2000))
.subscribe((data) => {
console.log(`延迟 2s 输出`, data);
});
timeout
在指定的间隔内,如果不发出值就报错
function makeRequest(time: number) {
return of("Request Complete!").pipe(delay(time));
}
of(4000, 3000, 2000)
.pipe(
concatMap((time) =>
makeRequest(time).pipe(
timeout(2500),
catchError((err) => of(`Request Error ${err}`))
)
)
)
.subscribe((val) => {
console.log(val);
});
结果:
finalize
当 Observable 完成时调用函数。
function makeRequest(time: number) {
return of("Request Complete!").pipe(delay(time));
}
of(4000, 3000, 2000)
.pipe(
concatMap((time) =>
makeRequest(time).pipe(
timeout(2500),
catchError((err) => of(`Request Error ${err}`))
)
),
finalize(() => {
console.log(`finalize`);
})
)
.subscribe((val) => {
console.log(val);
});
结果:
条件和布尔操作符
every
如果完成时所有的值都能通过断言,那么发出 true,否则发出 false
of(1, 2, 3, 4, 5)
.pipe(every((val: number) => val % 2 === 0))
.subscribe((val) => {
console.log(`val`, val); // false
});
of(2, 4, 6, 8)
.pipe(every((val: number) => val % 2 === 0))
.subscribe((val) => {
console.log(`val`, val); // true
});
find
只发出源发出的满足某些条件的第一个可观察值。
of(1, 5, 10)
.pipe(find((val: number) => val % 5 === 0))
.subscribe((val) => {
console.log(`find ===>`, val); // 输出 5
});
findIndex
只发出由满足某些条件的可观测源发出的第一个值的索引。
of(1, 5, 10)
.pipe(findIndex((val: number) => val % 5 === 0))
.subscribe((val) => {
console.log(`find ===>`, val); // 输出 1
});
isEmpty
如果 Observable 在完成之前没有发出任何值,返回 true, 否则返回 false。
const source = new Subject<string>();
const result = source.pipe(isEmpty());
source.subscribe((x) => console.log(x));
result.subscribe((x) => console.log(x));
source.next("a");
source.next("b");
source.next("c");
source.complete();
结果:
defaultIfEmpty
如果在完成前没有发出任何通知,那么发出给定的值
import { EMPTY, of } from "rxjs";
import { defaultIfEmpty, every } from "rxjs/operators";
EMPTY.pipe(defaultIfEmpty("defaultValue")).subscribe((val) => {
console.log(val); // 输出 defaultValue
});
总结
粗略地总结了 rxjs 的一些基本使用,对于 rxjs 的介绍,这篇文章并不能完全覆盖到,后面会不断的更新,总结常见的 rxjs 用法和项目中的实战案例。