Angular学习笔记七之RxJs

一、概述

RxJs是一个专门用来处理异步操作的JavaScript库。RxJs提供了一个核心类:Observable、几个关键的类:Observer, Schedulers, Subjects。可以将RxJs想象成面向事件的Lodash
RxJs库是以纯函数为基础的,因此特性强大、功能稳定。
RxJs库拥有一整套在可观察者对象中控制事件流的算子,可以在事件流的各个阶段实现监听和控制。
RxJs中有几个关键的概念:

  1. 可观察对象(Observable)。可以类比为Promise对象,在其内部可以进行异步操作,并且在异步操作执行完毕后将结果传递到可观察对象的外部。
  2. 观察者(Observer)类比then方法中的回调函数,用于接收可观察对象传递出来的数据
  3. 订阅(Subscribe)类比then方法,当可观察对象发出数据时,订阅者可以接收到数据。

二、Observable

  1. 创建方式:通过new Observable()构造函数创建 ,接收一个方法作为参数。这个方法拥有一个订阅者参数,通过这个参数的next()方法可以向外发送数据。
const observable = new Observable((subscriber) => {
    
    
    subscriber.next(1);
})
  1. Observable对象中的代码是惰性的,区别于Promise对象,创建的同时立即执行;Observable对象只有被订阅才会执行其中的代码。
  2. 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);
  1. Observable对象的subscriber.next()方法可以被调用多次,每次调用都会被observer.next监听到。
  2. 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);

控制台输出:
在这里插入图片描述

  1. 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);

在这里插入图片描述

  1. 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');

在这里插入图片描述

  1. 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

BehaviorSubjectSubject的一个变体,它保存的是最新发送出去的数据,即“当前值”。当有观察者订阅它的时候,获取到的就是最新发送出去的数据。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

fromEvent

public static fromEvent(target: EventTargetLike, eventName: string, options: EventListenerOptions, selector: SelectorMethodSignature): Observable

参数一:DOM元素或者NodeList
参数二:事件名称 clickmousedown
参数三:可选值,传递给事件监听函数的参数
参数四:可选值,函数处理结果,接收事件处理函数的参数,应该返回单个值

// 第三个参数是配置对象,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)
    })
}

点击之后,获取的是第二次请求的结果。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_45855469/article/details/130991565