一、概述
RxJs
是一个专门用来处理异步操作的JavaScript
库。RxJs
提供了一个核心类:Observable
、几个关键的类:Observer
, Schedulers
, Subjects
。可以将RxJs
想象成面向事件的Lodash
。
RxJs
库是以纯函数为基础的,因此特性强大、功能稳定。
RxJs
库拥有一整套在可观察者对象中控制事件流的算子,可以在事件流的各个阶段实现监听和控制。
RxJs
中有几个关键的概念:
- 可观察对象(
Observable
)。可以类比为Promise
对象,在其内部可以进行异步操作,并且在异步操作执行完毕后将结果传递到可观察对象的外部。 - 观察者(
Observer
)类比then
方法中的回调函数,用于接收可观察对象传递出来的数据 - 订阅(
Subscribe
)类比then
方法,当可观察对象发出数据时,订阅者可以接收到数据。
二、Observable
- 创建方式:通过
new Observable()
构造函数创建 ,接收一个方法作为参数。这个方法拥有一个订阅者参数,通过这个参数的next()
方法可以向外发送数据。
const observable = new Observable((subscriber) => {
subscriber.next(1);
})
Observable
对象中的代码是惰性的,区别于Promise
对象,创建的同时立即执行;Observable
对象只有被订阅才会执行其中的代码。Observable
对象的订阅需要调用observable.subscribe()
方法,类似于function.call()
函数调用。observable.subscribe()
方法接受一个对象作为参数,对象中包含可观察者对象发出数据后的回调函数。当Observable
对象执行了subscriber.next()
方法,就会触发observer.next
函数,observer.next
函数可以接收到subscriber.next()
方法的参数。
const observer = {
next: (value: any) => {
console.log(value);
}
}
observable.subscribe(observer);
Observable
对象的subscriber.next()
方法可以被调用多次,每次调用都会被observer.next
监听到。Observable
对象可以订阅多次,每订阅一次就会执行一次其中的代码。
例如如下代码,Observable
对象订阅了两个不同的观察者。
const observable = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
})
const observer1 = {
next: (value: any) => {
console.log("我是订阅者一号,接收到:"+value);
}
}
const observer2 = {
next: (value: any) => {
console.log("我是订阅者二号,接收到:"+value);
}
}
observable.subscribe(observer1);
observable.subscribe(observer2);
控制台输出:
observable.subscribe()
方法可以类比与function.call()
,都会执行函数的调用,获取函数执行的返回值。但call()
方法只能return
一次,只能接收一个同步事件的返回值,而subscribe()
方法可以next
多次,可以接收所有的同步和异步事件的返回值。
const observable = new Observable((subscriber) => {
subscriber.next(1);
setTimeout(function () {
subscriber.next(2);
})
})
const observer1 = {
next: (value: any) => {
console.log(value);
}
}
observable.subscribe(observer1);
Observable
对象中的代码是同步执行的
const observable = new Observable((subscriber) => {
subscriber.next(1);
setTimeout(function () {
subscriber.next(2);
})
})
const observer1 = {
next: (value: any) => {
console.log(value);
}
}
console.log('just before subscribe');
observable.subscribe(observer1);
console.log('just after subscribe');
Observable
对象中的subscriber
除了next
方法用来发送数据以外,还有两个方法:complete()
方法,用来告诉观察者发送数据完毕;error(errorMessage)
方法用来告诉观察者其内部发生了错误。与之对应,observer
观察者除了next
属性,还有complete
属性和error
属性,分别指向complete
事件和error
事件的回调函数。complete
方法不能传参。另外,无论是subscriber.complete()
方法还是subscriber.error(errorMessage)
方法都意味着观察结束,后续再使用subscriber.next()
发送数据,就不能被观察者观察到了。
const observable = new Observable((subscriber) => {
subscriber.next(1);
subscriber.complete();
subscriber.next(2); // 不能输出了
})
const observer1 = {
next: (value: any) => {
console.log(value);
},
complete: () => {
console.log('complete');
}
}
observable.subscribe(observer1);
const observable = new Observable((subscriber) => {
subscriber.next(1);
subscriber.error('发生了一个致命的错误');
subscriber.next(2);
})
const observer1 = {
next: (value: any) => {
console.log(value);
},
error: (error: any) => {
console.log(error);
}
}
observable.subscribe(observer1);
三、Subject
通过Observable
构造函数创建出来的可观察者对象,每订阅一个观察者,就会执行一次其中的代码。而使用Subject
创建出来的可观察者对象,是一个空的可观察对象,订阅的同时不会立即执行,需要手动调用这个可观察对象向外发出数据。和广播相关的场景可以使用这种可观察对象。可以在多个地方订阅这个可观察对象,在获取数据之后调用next
方法,这时所有订阅者都会同时接收到这个数据。使用Subject
创建出来的可观察对象是多播的,允许值传递给多个观察者;使用Observable
创建出来的可观察对象是单播的,每一个订阅者都拥有Observable
的独立执行。
// 创建一个空的可观察对象
const subject = new Subject();
// 有多个观察者订阅这个可观察对象
subject.subscribe({
next: (v) => console.log(`observerA: ${
v}`)
});
subject.subscribe({
next: (v) => console.log(`observerB: ${
v}`)
});
// 异步执行next方法获取数据,数据会被发送给所有的观察者
setTimeout(() => subject.next(1), 1000);
setTimeout(() => subject.next(2), 2000);
四、BehaviorSubject
BehaviorSubject
是Subject
的一个变体,它保存的是最新发送出去的数据,即“当前值”。当有观察者订阅它的时候,获取到的就是最新发送出去的数据。BehaviorSubject
适合用来表示“随时间推移的值”。BehaviorSubject
可以接收一个默认值,第一个观察者在订阅它的时候会立即执行,并且接收到这个默认值。
const behaviorSubject = new BehaviorSubject('默认值');
// 订阅的时候会立即执行一次,输出:ObserverA默认值
behaviorSubject.subscribe(value => {
console.log('ObserverA'+value)
})
// 执行并被ObserverA观察到,输出:ObserverA第一次改变值
behaviorSubject.next('第一次改变值')
// 执行,被ObserverA观察到,输出:ObserverA第二次改变值
behaviorSubject.next('第二次改变值')
// 又一个订阅,此时behaviorSubject保存的是最新发送出去的值:第二次改变值
// 虽然这个订阅在next执行之后,但是仍然可以获取behaviorSubject里面保存的最新的数据
// 输出:ObserverB第二次改变值
behaviorSubject.subscribe(value => {
console.log('ObserverB'+value)
})
五、ReplaySubject
ReplaySubject
保存所有的发送过的数据。ReplaySubject
记录 Observable
执行中的多个值并将其回放给新的观察者。当创建 ReplaySubject
时,你可以指定回放多少个值,就可以将最近的几个值都给新的观察者。
// 规定回放的次数为2.即当有新的订阅者的时候,
// 会查看历史发送数据,并把最近的2条数据发送给新的订阅者
const replaySubject = new ReplaySubject(2);
// 这个订阅者会接收到replaySubject的所有数据
replaySubject.subscribe(value => {
console.log(`replaySubjectA: ${
value}`)
})
replaySubject.next(1);
replaySubject.next(2);
replaySubject.next(3);
// 这个订阅者会接收之前的两条数据以及之后的所有数据
replaySubject.subscribe(value => {
console.log(`replaySubjectB: ${
value}`)
})
六、操作符
操作符是 Observable
类型上的方法,比如 .map(...)
、.filter(...)
、.merge(...)
,等等。操作符本质上是一个纯函数 (pure function)
。操作符是RxJs
库最有用的部分。操作符分为实例操作符和静态操作符。实例操作符是Observable
实例的方法,静态操作符是Observable
类的静态方法。最常见的静态操作符是创建操作符,用来创建Observable对象
。
1.of
of
用来创建简单的Observable
,发出给定的参数
const observableOf = of(1, '第二个元素', {
name: '第三个元素'});
observableOf.subscribe(
value => console.log(value)
)
2.from
从一个数组、类数组对象、Promise
、迭代器对象或者类 Observable
对象创建一个 Observable
。
const arr = [1, 2, 3];
// from创建一个Observable对象,依次将数组中的元素发射出去
const observable1 = from(arr)
observable1.subscribe((value) => {
console.log(`数字数组元素:${
value}`)
})
const promiseArr = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
// 依次将数组中的promise对象发射出去
const observable2 = from(promiseArr)
observable2.subscribe((value) => {
// value是每一个promise对象元素
value.then((value) => {
console.log(`Promise.then接收到的数据:${
value}`)
})
})
from
方法可以将Promise
对象转换为Observable
对象。promise
对象通过resolve
发送出去的数据会被观察者的next
回调函数接收到
// from方法可以将Promise对象转换为Observable对象
const promise = new Promise(resolve => {
setTimeout(() => {
resolve('promise resolved')
}, 1000)
})
const obs1 = from(promise)
obs1.subscribe(value => console.log(value))
3.fromEvent
public static fromEvent(target: EventTargetLike, eventName: string, options: EventListenerOptions, selector: SelectorMethodSignature): Observable
参数一:DOM
元素或者NodeList
参数二:事件名称 click
、mousedown
等
参数三:可选值,传递给事件监听函数的参数
参数四:可选值,函数处理结果,接收事件处理函数的参数,应该返回单个值
// 第三个参数是配置对象,once:true表示只执行一次
// 第四个参数是点击事件执行完毕的回调函数
const observable = fromEvent(document, 'click',{
once:true},(event)=>{
console.log(event)
console.log('执行了');
});
observable.subscribe((event) => {
// 如果fromEvent中传递了第四个参数,即点击事件执行完毕的回调函数,
// 那么这里的回调函数不会接收到任何参数
// 如果fromEvent中没有传递第四个参数,这里会接受到一个event对象
console.log(event)
console.log('document被点击');
})
4.map
map
作用类似于数组中的map
方法。
// 返回一个操作方法,接受一个observable对象作为参数
// 对observable对象发出的每一个值进行处理,返回一个新的observable对象
const operatorFunction = map((v: number) => v * v);
const result = operatorFunction(of(1, 2, 3));
result.subscribe(x => console.log(x));
模拟map
方法
// 模拟map方法
// 1.接受一个函数作为参数
// 2.返回值也是一个函数
// 3.返回的这个函数,需要接受一个observable对象作为参数,
// 4.返回值是一个新的observable对象,并且发出的数据是对observable参数发出的数据进行fn处理
function myMap(fn: (arg0: any) => void) {
return function (observable: any) {
return Observable.create((observer: any) => {
observable.subscribe((value: any) => {
observer.next(fn(value));
})
})
}
}
5.forkJoin
forkJoin
类似于Promise.all
方法。假设有多个Observable
对象在发出数据,forkJoin
可以等待所有Observable
对象发送数据完毕之后返回一个Observable
对象。通过forkJoin
组合的异步方法是并行执行的。
// 模拟axios请求
const getUsername =function (){
return new Promise(resolve => {
setTimeout(() => {
resolve('张三')
}, 1000)
})
}
const getAge =function (){
return new Promise(resolve => {
setTimeout(() => {
resolve(78)
}, 2000)
})
}
// forkJoin()接受一个对象,对象指向的是Observable对象发出的数据
// forkJoin()返回一个Observable对象。
// subscribe里面直接传递回调函数就表示对发出的数据执行这个函数
forkJoin({
username: from(getUsername()),
age: from(getAge())
}).subscribe(console.log)
6.pluck
pluck
如果Observable
对象发出的数据是对象,可以使用pluck
获取对应的属性。接受一个字符串作为参数,对发出的数据遍历,获取对应属性。
const observable = new Observable(subscriber => {
subscriber.next({
name: 'Brian'})
subscriber.next({
name: 'Joe'})
subscriber.next({
name: 'Sue'})
})
const pluckFn = pluck('name')
pluckFn(observable).subscribe(console.log)
7.interval
每个一段时间发送一个递增的数据。接受一个时间间隔作为参数。
interval(1000).subscribe((value) => {
console.log(value);
})
每隔1000ms发送一个数据
8.pipe()
在 RxJS
库中,pipe
是一个函数式编程的方法,用于将多个操作符串起来,以实现对 Observable
数据流的转换。由于 RxJS
中出现了许多数据变换和操作符,而直接嵌套这些操作会使代码难以阅读和维护,因此可以使用 pipe
方法将操作符组合成一条管道。类似于lodash
中的函数组合,以下代码相当于先对of(1,2,3)
执行map
处理,再执行filter
处理,最终返回一个处理后的新的Observable
。
of(1,2,3)
.pipe(
map(x => x * 2),
filter(x => x > 3)
).subscribe(console.log)
9.switchMap
switchMap
该 Observable
发出由源 Observable
发出的每项应用投射函数后的结果,并只接收最新投射的内部 Observable
的值。
let observable = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 1000);
})
const newObservable = observable.pipe(switchMap(value => {
return of(value + '被switchMap了');
}))
newObservable.subscribe(console.log);
10.take
接收一个数字作为参数,返回数据流中的前几个。
ngOnInit(): void {
of(1,2,3)
.pipe(
take(2)
).subscribe(console.log)
}
11.takeWhile
接受一个条件函数作为参数,返回满足条件的数据流。从前往后查找,遇到不满足条件的会直接停止。
of(1,100,3)
.pipe(
takeWhile(value => value < 10)
).subscribe(console.log)
12.takeUntil
接受一个可观察对象作为参数,当这个可观察对象发出数据的时候,主数据源就会停止数据发送。
// 创建一个异步发出数据的Observable
const observable= new Observable(subscriber => {
setTimeout(() => {
subscriber.next(1)
},5000)
})
// 使用Interval创建一个Observable
// 当oberable发出数据时,使用takeUntil停止
interval(1000)
.pipe(takeUntil(observable))
.subscribe(console.log)
控制台打印4个数字就会停止打印
13.throttleTime
节流。当可观察事件高频向外发送数据时,为避免频繁触发订阅回调,使用throttleTime
操作符,传递一个时间间隔作为参数,保证在规定的时间间隔内只发出一次数据。以下代码,在页面中频繁点击,1s内只会触发一次订阅者的回调函数。
fromEvent(document, 'click')
.pipe(throttleTime(1000))
.subscribe(() => console.log('Clicked!'));
14.debounceTime
防抖。接受一个时间间隔作为参数,只相应这段时间间隔中的最后一次数据的发送。如果在此时间间隔内事件被再次触发,就不会触发订阅者的回调函数。只有在一定时间间隔内,事件只触发一次的情况下,才会触发订阅者的回调函数。以下代码,在页面中频繁点击,只有1s的时间内只点击了一次的情况下才会触发订阅者的回调函数。
fromEvent(document, 'click')
.pipe(debounceTime(1000))
.subscribe(() => console.log('Clicked!'));
节流适用于用户点击的场景,因为点击的同时是期望立马有效果的,为了防止重复点击,加上节流。防抖适用于用户输入的场景,延迟回调函数的执行,因为输入事件可能还没有结束。所以说节流适用于一次操作就生效的场景,防抖适用于多次操作获得结果的场景。
15.distinctUntilChanged
distinctUntilChanged
不需要接收参数。检测本次发出的数据和上一次数据是否相同。如果相同则跳过,如果不同再传递给订阅者。
of(1, 1, 2, 2,3)
.pipe(distinctUntilChanged())
.subscribe(console.log);
七、案例一:鼠标拖拽元素
组件模版中
<style>
#box{
width: 100px;
height: 100px;
background-color: pink;
position: absolute;
left: 0;
top: 0;
}
</style>
<!--使用#标识需要在组件类中获取的html元素-->
<div id="box" #box></div>
组件类中
// 实现元素拖拽
// 1.鼠标按下的时候,计算鼠标相对于box的位置,即鼠标相对于视口的位置-box相对于视口的偏移量
// 2.为document增加mousemove事件,并计算鼠标相对于视口的位置:clientX和clientY
// 3.鼠标移动的时候,计算鼠标相对于视口的位置:clientX和clientY
// 4.设置box的偏移量:鼠标相对于视口的位置-鼠标相对于box的位置
// 5.鼠标抬起的时候,移除mousemove事件
// 使用ViewChild获取dom元素
@ViewChild('box') box: ElementRef | undefined;
ngAfterViewInit() {
// 移动事件
const moveObservable = fromEvent(document, 'mousemove')
.pipe(takeUntil(fromEvent(document, 'mouseup')))
// this.box.nativeElement为dom元素。?为可选链,防止报错
// fromEvent为dom元素绑定事件并将事件转为Observable
fromEvent(this.box?.nativeElement, 'mousedown').pipe(
// 处理数据的操作都放在操作符中
map(event => ({
// 获取鼠标相对于box的位置
distanceX: (event as MouseEvent).clientX - this.box?.nativeElement.offsetLeft,
distanceY: (event as MouseEvent).clientY - this.box?.nativeElement.offsetTop
})),
// 流式操作,这里获取的是map处理后的结果
// 使用对象结构赋值直接获取distanceX和distanceY
switchMap(({
distanceX, distanceY}) =>
fromEvent(document, 'mousemove').pipe(
// 当鼠标抬起时停止移动事件数据的发送
takeUntil(fromEvent(document, 'mouseup')),
map(moveEvent => ({
left: (moveEvent as MouseEvent).clientX - distanceX,
top: (moveEvent as MouseEvent).clientY - distanceY
})
)
)))
// 使用解构赋值直接获取left和top
.subscribe(({
left, top}) => {
this.box!.nativeElement.style.left = left + 'px';
this.box!.nativeElement.style.top = top + 'px';
})
}
八、案例二:搜索
组件模版中
<input type="text" placeholder="请输入搜索内容" #search>
组件类中
@ViewChild('search') search: ElementRef | undefined;
ngAfterViewInit() {
fromEvent(this.search?.nativeElement, 'input')
.pipe(
debounceTime(1000), // 防抖。适用于输入框,防止用户输入过快
map((event: any) => event.target.value), // 获取输入的内容
distinctUntilChanged(), // 过滤掉重复的值。如果用户删除之后快速输入,和上次的值一样,就阻止触发请求
switchMap((value: string) =>
this.response(value) // switchMap应该返回Observable,但是这里返回Promise也正常运行了,应该是能自动转成Obervable
)
)
.subscribe((res: any) => {
console.log(res)
}
)
}
// 模拟一个请求
private response(data: any) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
data: data,
status: 200
})
}, 1000)
})
}
九、案例三:串行请求
点击一个按钮,需要依次发送两个请求,第二个请求的参数需要依赖第一个请求的返回值
组件模版
<button #btn>点击获取数据</button>
组件类
@ViewChild('btn') btn: ElementRef | undefined;
ngAfterViewInit() {
fromEvent(this.btn?.nativeElement, 'click')
.pipe(
concatMap(event=>this.response('Hello')),
pluck('data'),
concatMap((data: any) => this.response(data + ' Angular'))
)
.subscribe((res: any) => {
console.log(res)
}
)
}
// 模拟一个请求
private response(data: any) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
data: data,
status: 200
})
}, 1000)
})
}
点击之后,获取的是第二次请求的结果。