本文介绍 RxJS 在 Angular 中的使用。RxJS 在 Angular 中的应用方向大抵可以分成下面几种:
- 使代码变得更清晰
- 严格的响应模式
- 改进状态管理
- 合并 RxJS 流
- 对用户动作作出反应
- 减少订阅代码
- 提升页面性能
1. 名称的由来
RxJS 可以看成是 Reactive Extensions for Javascript 的简写。你可以在 React Vue 中使用 RxJS.
2. 什么是 RxJS
RxJS is a library for composing asynchronous (observing and reacting to data) and event-based programs by using observable sequences.
3. 相比其他解决方案
- 比回调更加好管理
- 比 Promise 和 async/await, RxJS 可以取消,并且不是一次性的
- 而 RxJS 的优势在于:
4. RxJS 在 Angular 中的常见用途
5. 如何实现响应式?
所谓响应式,就是 z = x + y; 如果 x = 3; y = 4; 那么 z 此时就是 7. 但是某个时候 x 的值变成了 6, 那么此时如果不采用响应式,那么 z 的值仍然为 7, 而如果是响应式的,那么 z 的值就是 10.
响应式对于 Angular 应用的意义在于:
5.1 使用 getter 实现响应式
问题在于,无法将变化通知到整个 component.
5.2 使用事件回调实现响应式
问题同样在于,无法将变化通知到整个 component.
5.3 使用 RxJS 实现响应式
只要订阅了被观察对象,那么被观察对象一旦发生变化,所有订阅者都能收到消息。
举个例子,比如购物车总价由单个商品价格叠加而来。我们可以将每一个商品的价格看成单独的流,而总价就是这些流的 compose, 这样做之后,任意商品的价格发生变化,都会自动触发 compose 之后的流的刷新。
6. 基础概念
关于 Observable 有以下必备的概念:
- Observer / Subscriber
- Observable
- Subscribing
- Unsubscribing
- Creation functions
7. 什么是观察者
The observer is a collection of callbacks that knows hwo to listen to values delivered by the Observable.
An observer is a consumer of values delivered by an Observable.
- next
- error
- complete: 只有这个回调没有形参
8. 可观察对象能够提供什么样的数据?
不仅如此,从另外的角度;Observable 不仅可以作为有限的源,也可以作为随时间推移不断提供数据的无限的源。
9. 快速订阅
订阅一个可观测对象不一定要传入一个实现了 error next complete 接口的对象,实际上只传入一个参数也是可以的,只不过此时只会去处理 next 事件。
10. 取消订阅
需要遵循这样的一个订阅原则:subscribe 和 unsubscribe 必定是成对出现的;这是因为在合适的时机取消订阅有利于避免内存泄漏。
取消订阅的几种方法:
关于最后一种方法,示例代码如下:
11. of 和 from 的区别
操作符 of 和 from 的区别就是对待传入的参数是不同的。通常我们会使用一些内部封装好的方法来构建 Observable 对象,而不是通过直接 new 一个实例的方式。
12. 给 DOM 元素绑定事件
注意这里 @viewChild 和 fromEvent 的联合使用。
12. 使用 interval 创建无限源
一定要记住的是
13. 在线创建前端项目
使用 StackBlitz 网站在线创建前端项目:
14. RxJS 中的操作符
关于操作符你需要知道的:
- 操作符的本质是一个函数。
- 使用操作符对源提供的数据进行变形、转换、操作
- 使用 pipe 操作符可以将其它操作符串联起来使用
- 你可以在 rxjs.dev 中查看各个操作符的使用方法
15. tap 操作符
tap 操作符又称工具操作符。 与 map 等操作符不同在于,tap 操作符的主要作用就是为了做副作用的,所以它不需要返回值,因为它的输出强制和输入一致。所以 tap 一般用来打印日志。
16. take 操作符
如下图所示,这张图中 6 不会被发出来。请一定要注意 6 不会被发送出来。并不是发送出来之后没有人接受,而是根本不会被 emit 出来!
一个有趣的例子:
以及最基础的三个操作符:
17. map 操作符的内部原理
下面短短几行代码就可以展示出 map 操作符内部的原理。
你可以在网站 github.com/ReactiveX/r… 上找到这些操作符的源码。
18. Angular 中的 Reactive
大概是有三个层面:
- 使用 Observable 对象。
- 采用 RxJS 操作符。
- 对 action 进行反应。
那有什么好处呢?
- 提升性能
- 处理状态
- 对用户操作做出反应
- 简化代码
19. Observable 的错误处理
在 RxJS 中处理错误的步骤是:捕获错误-选择性的重新抛出错误-将发生错误的可观测对象换成新的可观测对象。
RxJS中的throwError创建函数用于创建一个不发射任何数据的Observable(被观察者),而是立即发出一个错误通知。其使用方式为throwError(()=> err),通过这个函数,可以传播一个错误,或者在需要立即抛出错误的情况下使用,这可以作为一种替代直接使用throw语句的方式。这个函数在需要中断数据流并立即通知错误时非常有用。
19.1 使用 throwError 抛出错误
方式一:直接使用throwError函数
这种方式中,我们直接使用throwError函数创建一个Observable,该Observable会立即发出一个错误。
import { throwError } from 'rxjs';
// 创建一个会立即发出错误的Observable
const errorObservable = throwError(new Error('这是一个错误消息'));
// 订阅这个Observable,并处理错误
errorObservable.subscribe(
value => {
// 这里不会执行,因为Observable没有发射值
},
err => {
console.error('捕获到错误:', err.message); // 将输出:捕获到错误: 这是一个错误消息
},
() => {
// 完成回调,这里不会执行,因为Observable发射了错误
}
);
方式二: 在 observable 链中使用 throwError
const { catchError, map, switchMap } = require("rxjs/operators")
const { of, throwError, interval } = require("rxjs")
// 模拟一个可能会出错的数据获取函数
function getData() {
if (Math.random() > 0.5) {
return of('成功获取数据');
} else {
return throwError(() => new Error('数据获取失败'));
}
}
// 错误处理函数
function handleError(error) {
console.error('\n捕获到错误:', 'error');
return of('使用默认数据\n'); // 在出错时返回一个默认的Observable
}
// 创建一个每隔1秒发出值的Observable,并使用switchMap来调用getData函数
interval(1000).pipe(
switchMap(() => getData().pipe(
map(data => `处理后的数据: ${data}`),
catchError(handleError) // 使用catchError来捕获并处理getData可能抛出的错误
))
).subscribe(
result => console.log(new Date().toISOString(), result), // 输出当前时间和处理后的数据或默认数据
err => console.error('不应该执行到这里,因为错误已经被catchError处理了'),
() => console.log('完成') // 这个完成回调实际上在这个无限流中不会被调用
);
这个无限流会一直持续下去,因为订阅者的 error 不会被调用!
19.2 使用 throw 抛出错误
const { catchError, map } = require("rxjs/operators")
const { of, throwError } = require("rxjs")
of(2, 4, 6, 8).pipe(map(i => {
if (i === 6) {
throw 'Error'
}
return i
}),
catchError(err => of('six'))
).subscribe({
next: x => console.log(x), // 2, 4, six 没有 8
error: err => console.log('err:', err), // 这里不会打印
})
catchError 的意义:
在动作流中,一个常见的问题是错误处理。如果动作流中出现未处理的错误,流将自动停止,不再发射任何数据。为了防止这种情况导致整个流的中断,最佳实践是使用错误处理操作符,比如
catchError,来捕获这些错误。
19.3 发生错误的时候使用默认值
默认值一般来自下面三个地方:
- 硬编码在代码中。
- 空值,例如
[] ''等 - 返回 RxJS 提供的常量,如 EMPTY
20. 使用 async 管道
如下图所示是使用和不使用 async 管道的模板。使用 async 管道的好处是显而易见的。
- 可以简化 class 中的代码,不需要在 class 中订阅和取消订阅 observable, 因此可以省略大量的钩子函数的编写。
- 可以将数据获取由命令式改成声明式,但此时数据的格式就变成了
Observable<T> | undefined。如下图所示。
21. EMPTY 常量
RxJS中的EMPTY是一个Observable,它不发出任何数据项,而是立即发出完成通知。它用于返回一个空的Observable。
代码示例:
const { catchError, map, switchMap } = require("rxjs/operators")
const { of, throwError, interval, EMPTY } = require("rxjs")
// 使用EMPTY返回一个不发出任何值的Observable
const emptyObservable = EMPTY;
// 订阅该Observable
emptyObservable.subscribe(
value => console.log(value), // 不会执行,因为EMPTY不发出值
error => console.error(error), // 不会执行,没有发生错误
() => console.log('Completed!') // 会执行,因为EMPTY会立即完成
);
在上面的代码中,我们导入了RxJS中的EMPTY常量,并创建了一个名为emptyObservable的变量来引用它。当我们订阅这个Observable时,它不会发出任何值,因此观察者的 next 不会响应,而是会立即触发completed回调。因此,控制台将只打印出"Completed!",而不会打印出任何值。
在RxJS的Observable事件流中,EMPTY 常量可以在需要立即终止流并发送完成通知的情况下使用。例如,当某个条件不满足,你希望流不继续发射数据而是直接完成时,就可以返回 EMPTY。
以下是一个简单的例子,展示了如何在Observable事件流中使用 EMPTY:
const { catchError, map, switchMap } = require("rxjs/operators")
const { of, throwError, interval, EMPTY } = require("rxjs")
// 假设有一个函数,它根据条件返回一个Observable
function conditionalObservable(condition) {
if (condition) {
// 如果条件为真,返回一个发出特定值的Observable
return of('Data emitted because condition is true');
} else {
// 如果条件为假,返回一个EMPTY Observable
return EMPTY;
}
}
// 模拟一个事件流
const eventStream = of(true, false, true, false, true);
// 在事件流中根据每个事件的值来决定是否发出数据或者立即完成
const processedStream = eventStream.pipe(
switchMap(condition => conditionalObservable(condition))
);
// 订阅处理后的流
processedStream.subscribe(
value => console.log(value), // 当条件为真时,打印发出的值
error => console.error(error), // 错误处理
() => console.log('Stream completed!') // 流完成时打印
);
/*
Data emitted because condition is true
Data emitted because condition is true
Data emitted because condition is true
Stream completed!
*/
在这个例子中,conditionalObservable 函数根据传入的 condition 参数来决定是返回一个发出特定数据的Observable还是返回一个 EMPTY Observable。在 eventStream 中,我们模拟了一个发出布尔值的事件流。通过使用 switchMap 操作符,我们根据每个事件的值来调用 conditionalObservable 函数,并返回相应的Observable。如果 condition 为真,则发出一个数据项;如果为假,则返回一个 EMPTY Observable,它不会发出数据而是直接完成。这样,在最终订阅的 processedStream 中,我们只会看到条件为真时发出的数据项,并在流结束时收到完成通知。
总结一下
EMPTY 是 rxjs 提供的常量,本质上是一个 observable,不会引起观察者 next error 回调的触发,因此可以作为 catchError 的搭配,防止错误发生之后影响整个流程断掉。但在某些情况下,作为最后一个可以触发 complete.
不过除了返回 EMPTY, 如果真的想让后续流程知道确实发生了一次内容推送,那么可以返回一个空值,如下所示。
而在 return EMPTY 的上方,可以看到,错误信息已经通过其他方式传递出去了。
使用 EMPTY 常量可以更好的配合声明式编程方式,如下图所示:
22. Angular 中检测是否需要更新视图的策略
Angular uses change detection to track changes to application data so that it knows when to update the Ul
22.1 设置不同的更新策略
Change Detection Strategies是Angular框架中用于优化性能的一个重要机制。Angular提供了几种不同的变化检测策略,其中最常用的是Default和OnPush。
Default策略是Angular的默认设置,它使用checkAlways策略。这意味着每当Angular检测到任何变化时,每个组件都会被检查。这种策略简单且易于理解,但在大型应用中可能会导致性能问题,因为它会频繁地触发变化检测,即使很多组件实际上并没有发生变化。
相比之下,OnPush策略通过最小化变化检测周期来提高性能。在OnPush策略下,组件只在以下情况下被检查:当组件的@Input属性发生变化时,当组件触发事件时,或者当绑定的Observable发出值时。这意味着如果组件的输入属性没有变化,并且没有触发任何事件或Observable发出,那么Angular就不会对该组件进行变化检测,从而节省了计算资源。
下面是一个使用OnPush策略的Angular组件的示例代码:
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
@Input() products: any[];
// 组件的其他逻辑...
}
在这个例子中,ProductListComponent组件使用了OnPush策略。这意味着除非products输入属性发生变化,或者有事件被触发,或者绑定的Observable发出新的值,否则Angular不会对这个组件进行变化检测。这有助于减少不必要的计算,提高应用的性能,特别是在大型和复杂的应用中。
22.2 使用不同的更新策略造成的差异
在Angular中,要展示 ChangeDetectionStrategy.Default 和 ChangeDetectionStrategy.OnPush 策略下的不同行为,我们可以通过修改父组件和子组件的配置来实现。以下是如何操作的示例:
首先,让我们定义一个简单的父子组件结构,其中父组件每秒更新一个计数器的值,并将其传递给子组件。
父组件(ParentComponent)
// parent.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-parent',
template: `<app-child [data]="counter"></app-child>`,
// 初始设置为 Default 策略
changeDetection: ChangeDetectionStrategy.Default
})
export class ParentComponent {
counter = 0;
constructor() {
setInterval(() => {
this.counter++; // 每秒递增,触发 Default 策略的变化检测
}, 1000);
}
}
子组件(ChildComponent)
// child.component.ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-child',
template: `<p>{{ data }}</p>`,
// 初始设置为 Default 策略
changeDetection: ChangeDetectionStrategy.Default
})
export class ChildComponent {
@Input() data: number;
}
在这种情况下,即使我们每秒递增 counter 的值,由于 Default 策略,子组件的视图会实时更新显示最新的计数。
现在,我们将子组件的策略更改为 OnPush 并观察行为的变化:
// child.component.ts (修改后的版本)
import { Component, Input, ChangeDetectionStrategy, SimpleChanges,OnChanges } from '@angular/core';
@Component({
selector: 'app-child',
template: `<p>{{ data }}</p>`,
// 更改为 OnPush 策略
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnChanges {
@Input() data: number;
ngOnChanges(changes: SimpleChanges) {
if (changes.data) {
console.log('Data changed to:', changes.data.currentValue);
}
}
}
在 OnPush 策略下,子组件不会自动检测到 counter 值的更新,因为基本数据类型的引用没有改变。为了强制更新,我们可以在父组件中使用不同的方法来确保引用的变化,例如使用一个对象来包装 counter 值:
父组件的更新策略
// 使用对象包装 counter 值
this.counterObj = { count: this.counter };
子组件的更新
// 现在 data 是一个对象
@Input() data: { count: number };
ngOnChanges(changes: SimpleChanges) {
if (changes.data) {
console.log('Data changed to:', changes.data.currentValue.count);
}
}
通过这种方式,每次 counter 更新时,我们实际上是在更改 counterObj 对象的引用,这会触发 OnPush 策略下子组件的变化检测。
请注意,为了演示两种策略的不同行为,你需要在实际应用中根据需要切换父组件和子组件的 ChangeDetectionStrategy 设置。在实际开发环境中,你不会同时在同一个组件中使用两种策略,而是根据组件的特定需求选择一种策略。
23. 结合实例说明 RxJS 与声明式编程
下面的代码展示了声明式编程的基本思想。
import { Component, OnInit } from '@angular/core';
import { ProductCategory } from './product-category'; // 假设存在这个类型
import { ProductService } from './product-service'; // 假设存在这个服务
import { EMPTY } from 'rxjs'; // 导入 EMPTY 常量
import { catchError } from 'rxjs/operators'; // 导入 catchError 操作符
@Component({
templateUrl: "./product-list.component.html", // 补充双引号
styleUrls: ['./product-list.component.css'] // 补充闭合的双引号和分号
})
export class ProductListComponent implements OnInit {
pageTitle = 'Product List';
errorMessage: string | null = null; // 初始化 errorMessage
categories: ProductCategory[] = [];
products$: Observable<Product[]> | undefined; // 假设存在 Product 类型
constructor(private productService: ProductService) {}
ngOnInit(): void {
this.products$ = this.productService.getProducts()
.pipe(
catchError(err => {
this.errorMessage = err.message; // 假设错误对象有 message 属性
return EMPTY; // 返回 EMPTY 以结束 Observable 链
})
);
}
}
这段代码展示了声明式编程的几个关键方面:
-
组件声明:使用
@Component装饰器声明了一个Angular组件,包括它的模板和样式表。 -
属性初始化:组件的属性(如
pageTitle、errorMessage、categories和products$)在类中被明确初始化。 -
依赖注入:通过构造器注入
ProductService,这是Angular依赖注入系统的声明式使用。 -
响应式编程:使用 RxJS 的
Observable和pipe方法来声明如何处理异步数据流(getProducts方法返回的products$)。 -
错误处理:使用
catchError操作符声明性地处理可能发生的错误,而不是使用传统的 try-catch 块。 -
生命周期钩子:通过实现
Oninit接口和ngOnInit方法,声明性地定义了组件初始化时的逻辑。 -
模板语法:在组件的模板文件(
product-list.component.html)中,将使用Angular的模板语法来声明性地绑定数据和行为。
声明式编程的核心思想是描述“是什么”而不是“怎么做”,上述代码通过声明组件的属性、行为和数据处理方式,而不是编写命令式的步骤来实现功能,展示了这一思想。
24. RxJS 中结合数据流的三种操作符
- combineLatest
- forkJoin
- withLatestFrom
上述的三个操作符就是在 Angular 中操作多个数据流的操作符。它们能够对多个流进行操作,完成复杂的数据交互、转换。
24.1 为什么要将多个数据流结合起来?
将多个数据流结合起来能够完成更加复杂的功能,同时也体现出了 RxJS 的强大之处。优势在于:
24.2 结合操作符是如何改变数据的?
24.3 combineLatest 操作符原理图示
总结就是,所有的数据流的地位都是平等的,先等待所有的数据流都至少 emit 一次之后。如果任意数据流有新的 emit 就更新返回的组合值。
24.4 forkJoin 操作符原理图示
总结就是,forkJoin 会等待所有的数据流都 emit 完成之后,再将每一个数据流的最后一次 emit 组合起来当成返回值。注意它只 emit 一次,这一点和 combineLatest 是不同的。它适合有限流不能用于无限流。
forkJoin用于等待所有给定的 Observables 完成,并将它们的最新值作为数组一起发出。它适用于并行数据获取,但不应与不完成的 Observables 一起使用,例如动作流。
24.5 withLatestFrom 操作符原理图示
总结一下,withLatestFrom 和 combineLatest 比较相似,唯一的不同会体现在触发时机上。后者是每一个流 emit 之后都要返回新的组合值。而前者的各个数据流之间地位是不同的,只有主流 emit 之后才会重新计算组合值。组合值也是取所有组成数据流的最新值。
RxJS的withLatestFrom操作符是一种组合操作符,它接收一组Observables并订阅它们。仅当源Observable发出一个 item, 且所有其他Observables至少已发出一次时,它才会向输出Observable发出一个值。发出的值将每个输入Observable的最新发出值组合成一个数组。当源Observable完成时,它也会完成。这个操作符适用于需要基于多个数据流的最新值来做出响应的场景。
24.6 使用组合操作符完成实际需求
实际需求就是根据接口返回的 id 值去另外一个接口返回值中查找得到此 id 对应的 name, 实现代码如下所示:
import { combineLatest, map } from 'rxjs'; // 导入所需的 RxJS 函数
import { Product } from './product'; // 假设存在 Product 类型
// ...
products$ = this.http.get<Product[]>(this.productsUrl)
.pipe(
tap(data => console.log('Products:', JSON.stringify(data))),
catchError(this.handleError)
);
productsWithCategory$ = combineLatest([
this.products$,
this.productCategoryService.productCategories$
]).pipe(
map(([products, categories]) =>
products.map(product => ({
...product,
price: product.price ? product.price * 1.5 : 0,
category: categories.find(c => product.categoryId === c.id)?.name,
searchKey: [product.productName]
}))
)
);
这段代码使用 combineLatest 操作符来结合两个 Observables:productsS 和 productCategoriesS。当这两个 Observables 中的任意一个发出一个新的值时,map 操作符会创建一个新的数组,其中包含原始 products 的每个元素,但价格增加了50%,并且添加了 category 属性,这个属性是从 categories 中找到的与 product.categoryId 相匹配的分类的名称。最后,使用 as Product 确保结果对象被识别为 Product 类型。
上面的代码展示出了如何同时操作两个数据流的方式;实际上是如何同时结合两个接口返回值来完成一些功能,这个解决方案看起来既实用又简洁。
上面的数据我们使用了数据接口 Product 进行了约束,这个接口的形式为:
export interface Product {
id: number; // 产品的唯一标识符
productName: string; // 产品的名称
productCode?: string; // 产品代码(可选)
description?: string; // 产品描述(可选)
price?: number; // 产品价格(可选)
categoryId?: number; // 产品所属分类的唯一标识符(可选)
category?: string; // 产品所属的分类名称(可选)
quantityInstock?: number; // 库存中的产品的量(可选)
searchKey?: string[]; // 用于搜索的关键字数组(可选)
supplierIds?: number[]; // 供应此产品的供应商的唯一标识符数组(可选)
}
注意这个接口里面的可选属性,这很重要!
25. 将数据流和事件流绑定在一起
将数据流和事件流绑定在一起可以完成大部分的前端开发任务,特别是在表单相关操作中,例如点击列表进入详情页面、增加 item 等,总之非常的实用。
下面两个图说明了如何将数据流和事件流绑定在一起的原理。
不难看出来,本质上是利用了 combineLatest 操作符的特点。也就是数据流一般作为只 emit 一次的固定流,或者有限流。而事件流作为可以多次 emit 的动态流,或者称为无限流。这样做之后可以实现,对于一段时间内的所有事件都能保证它们使用的数据是固定的。
26. 单向数据流
Observable是只读的数据流,订阅者可订阅以响应其通知。观察者(Observer)只能观察和作出反应,不能向Observable中发射任何数据,只有Observable的创建者才能发射数据项。
然而:
Subject是一种特殊类型的Observable,它实现了Observer接口,因此既可以发射数据,也可以订阅和响应数据。
虽然 Subject 既可以接受也可以发送,但是它一般都是用来发送的。
除此之外,Subject 是广播的。意思就是对于一个特定的 Subject 它可以有很多订阅者,而普通的 observable 对象一般只有一个订阅者。
-
Observable is generally unicast:
- Observable是单播的,意味着每个订阅者都会收到独立的数据流。
- 就像水管一样,每次打开都会形成一条新的水流,各条水流之间互不影响。
- 在网络中,这类似于“单播”通信,信息只在两个节点之间传递。
-
Subject is multicast:
- Subject则是多播的,允许数据被多播到多个观察者。
- 它可以被看作是一个部分设计好的“水管蓝图”,多个订阅者可以连接到这个“蓝图”上,然后同步接收数据。
- 在网络中,这类似于“多播”通信,数据可以一次性发送给多个目标节点。
因此,Observable和Subject的主要区别在于数据流的传播方式:Observable是单播,为每个订阅者提供独立的数据流;而Subject是多播,允许多个订阅者同步接收相同的数据。
下图形象的说明了 Multicast 的工作流程。
下面展示 Subject 在项目实践中的应用,可以用来批量取消数据订阅,防止内存泄漏:
...
private isComponentDestroyed = new Subject<void>();
...
constructor(
...
) {
fromEvent(window, 'click')
.pipe(
takeUntil(this.isComponentDestroyed)
)
.subscribe((event) => {
...
});
}
...
ngOnDestroy() {
this.isComponentDestroyed.next();
this.isComponentDestroyed.complete();
}
只要我们把 takeUntil(this.isComponentDestroyed) 放在无限流的 pipe 中,就可以在组件销毁的时候一次性清除它们。
27. Behavior Subject
BehaviorSubject是一种特殊的Subject,它会缓存最近发出的值,并将其发送给任何后续订阅者。若尚未发出任何数据,它会发出一个默认值。例如,创建一个新的 BehaviorSubject 并设定数字8为默认值,即aSub = new BehaviorSubject<number>(8);。这样,即使在新订阅者订阅时尚未有其他数据发出,新订阅者也会立即接收到默认值8,随后则能接收到最近发出的值。
参考如下的数据流:
这里第二行的 0 和第三行的 2 分别表示了初始的默认值和缓存的最近值。
BehaviorSubject 的意义在于,对 subscriber 友好一些,不至于其订阅之后直接尬在原地。
28. 创建一个事件流
首先,创建了一个Subject对象categorySelectedSubject,它用于在分类被选中时发出事件。Subject是 RxJS中的一个类,它同时是观察者(Observer)和被观察者(Observable),这意味着它既可以发出数据,也可以订阅数据。
private categorySelectedSubject = new Subject<number>();
接着,我们通过 asObservable() 方法将 Subject 转换为Observable,这样做是为了防止外部直接调用next()方法,确保数据流的安全性和单向性。
categorySelectedActionS = this.categorySelectedSubject.asObservable();
onSelected 方法用于处理分类选择事件。当分类被选中时,我们通过 next() 方法将选中的分类ID作为事件发出。注意,这里使用了 +categoryId 来确保ID是数值类型。
onSelected(categoryId: string): void {
this.categorySelectedSubject.next(+categoryId);
}
在HTML模板中,我们有一个<select>元素,它的change事件绑定了onSelected方法。当用户选择一个选项时,会触发onSelected方法,并传入选中选项的值(即分类ID)。
<select (change)="onSelected($event.target.value)">
<option
*ngFor="let category of categories$ | async"
[value]="category.id"
>
{{ category.name }}
</option>
</select>
注意,在*ngFor中,我们使用了categories$ | async来异步获取分类列表。这里假设 categories$ 是一个 Observable,它发出分类列表的数据。async 管道会自动订阅这个 Observable,并在数据发生变化时更新视图。
这段代码实现了一个简单的动作流:当用户从下拉列表中选择一个分类时,会触发 onSelected 方法,该方法通过 Subject 发出一个包含所选分类ID的事件。其他组件或服务可以订阅这个 Observable (即 categorySelectedActionS ),以便在用户选择分类时作出响应,比如更新产品列表。
完成的代码
其中,combineLatest 操作符是对事件流的响应。
总结
完成一个事件流需要通过如下的三个步骤:
- Create an action stream (Subject/BehaviorSubject)
- Combine the action and data streams
- Emit a value to the action stream when an action occurs
29. 巩固使用事件流完成表单交互
import { Subject, EMPTY } from 'rxjs';
import { combineLatest, map, catchError } from 'rxjs/operators';
import { ProductService, ProductCategoryService } from './services'; // 假设服务类在这里导入
export class ProductListComponent {
pageTitle = 'Product List';
errorMessage = ''; // 初始化错误消息为空字符串
private categorySelectedSubject = new Subject<number>();
categorySelectedAction$ = this.categorySelectedSubject.asObservable();
products$ = combineLatest([
this.productService.productsWithCategory$,
this.categorySelectedAction$
]).pipe(
map(([products, selectedCategoryId]) =>
products.filter(product =>
selectedCategoryId ? product.categoryId === selectedCategoryId : true
)
),
catchError(err => {
this.errorMessage = err;
return EMPTY;
})
);
categories$ = this.productCategoryService.productCategories$.pipe(
catchError(err => {
this.errorMessage = err;
return EMPTY;
})
);
constructor(private productService: ProductService, private productCategoryService: ProductCategoryService) {}
onAdd(): void {
console.log('Not yet implemented');
}
onSelected(categoryId: string): void {
this.categorySelectedSubject.next(+categoryId);
}
}
代码的详细解释:
ProductListComponent是一个Angular组件类。pageTitle是组件的标题。errorMessage用于存储可能发生的错误信息。categorySelectedSubject是一个Subject,用于发送选择的分类ID。categorySelectedAction$是一个Observable,由categorySelectedSubject转换而来,供外部订阅。products$是一个Observable,它结合了产品流和分类选择动作流,根据选择的分类ID过滤产品列表。categories$是一个Observable,它获取产品分类列表,并处理可能的错误。- 构造函数注入了
ProductService和ProductCategoryService服务。 onAdd方法目前只输出一条消息,表示该方法尚未实现。onSelected方法接收一个分类ID字符串,并将其转换为数字后通过categorySelectedSubject发出。
30. 数据流的初始值
上面的代码有一个问题,从页面交互上来说我们必须手动的点击一个类别才能给 categoryId 赋值,否则就会因为 categoryId 不合法导致详细数据不可见。 常见的指定流的初始值的方法有两个:
- 使用 BehaviorSubject
- 使用 startWith 生成操作符
startWith 操作符的使用
startWith是一个组合操作符,它订阅输入的可观察对象(Observable)并创建一个输出的可观察对象。当被订阅时,它会同步发出所有提供的值。每当有项目发出时,该项目就会被传输到输出的可观察对象中。重要的是,初始值必须与输入可观察对象发出的类型相同。简而言之,startWith允许在订阅发生时立即发送一系列预设的初始值,之后才会继续传递来自原始可观察对象的值,这对于设置初始状态或预设值非常有用。这种操作符在处理流数据时可以提供更大的灵活性和控制力,确保观察者从一开始就能接收到所需的信息。
其作用原理如下图所示:
private categorySelectedSubject = new BehaviorSubject<number>(null); // 初始化一个BehaviorSubject,类型为number,初始值为null
categorySelectedAction$ = this.categorySelectedSubject.asObservable(); // 将Subject转换为Observable
// ...
// 假设在某个方法或者其他逻辑中
this.categorySelectedAction$
.pipe(
startWith(null) // 假设你想要在开始的时候发送一个null值
// ... 其他操作符
)
.subscribe(value => {
// 处理订阅的逻辑
});
改进之后的程序如下所示:
关于 Subject 和 BehaviorSubject 有两点需要说明。
在无需指定默认值的情况下使用 Subject, 在需要使用默认值的情况下使用 BehaviorSubject.
31. 样式相关
一旦我们的数据源变成了 Observable 格式的,那么我们就可以通过 async pipe 方便的处理它的内部数据。比如为一些特定的数据项通过类名的方式附加特殊的样式。
32. 异常相关
使用 observable 数据源的另外一大部分就是异常或者错误场景下的处理。这种情况下,一定要注意 catchError 和 EMPTY 常量的搭配使用。千万不要让错误溢出到 subscriber 中,如下所示:
显然,我们将错误信息通过其它的方式存储了起来,而没有在流程中去处理它。然后我们就可以将错误信息结合 *ngIf 展示出来了。
33. Observable 中的状态
在 Angular 中,什么才能称得上是状态?
- View State
- User Information
- Entity Data
- User Selection and Input
33.1 如何正确的更新状态
有两种更新状态的方式:后端方式和前端方式。
33.2 与状态更新息息相关的操作符名为 scan
RxJS中的scan操作符非常有用,它能在可观察对象(Observable)中累积项目。通过使用累加器函数,例如scan((acc, curr) => acc + curr),它会对每个发出的项目应用此函数。每当有新的项目发出时,累加器函数就会执行,将前一个累积值(accumulator)与当前项目相结合,产生新的累积结果。这个结果被缓存并发出,使得观察者可以追踪整个累积过程。scan操作符在封装和管理状态、计算总额以及将项目累积到数组中等方面特别有用。简而言之,scan允许我们观察一个累积过程,而不仅仅是单个的输出值,这在处理流式数据或需要跟踪状态变化的应用场景中非常有价值。
下图展示的是 scan 操作符的作用原理。
使用 scan 操作符,非常像 JS 中的 reduce 方法,也可以指定一个最初值,如下所示:
使用 scan 和想象力可以发挥出巨大的作用:
34. merge 操作符
RxJS的merge是一个创建函数,用于合并多个可观察对象(Observables)的发出值。不同于管道操作符,merge是一个静态创建函数,其使用方式为merge(aS, bS, cS)等,其中aS、bS、cS等代表不同的可观察对象。merge函数主要用于合并类似类型的序列,将这些序列发出的值融合在一起。通过这种方式,我们可以同时监听多个数据源,并在它们中的任何一个发出新值时获得通知。这在处理多个异步数据流,如用户输入、网络请求或传感器数据时特别有用。通过merge,我们可以简化代码结构,提高响应性,并确保不会遗漏任何重要的事件或数据更新。当所有通知都发送完成时, merge 之后的流也就结束了。
merge 操作符的作用原理如下所示:
在实际应用中,我们可以使用 merge 完成表单中新增任务,原理如下所示:
34.1 使用 merge/scan 完成 add 功能
// 从'rxjs'库中导入多个函数和类。这些是用于响应式编程的工具。
import { Subject, EMPTY, merge, combineLatest, of } from 'rxjs';
// 从'rxjs/operators'中导入操作符。这些是用于处理Observable流的函数。
import { map, catchError, scan } from 'rxjs/operators';
// 定义一个服务或组件类。
export class YourServiceOrComponent {
// 创建一个新的Subject实例,用于发布新产品插入事件。Subject是Observable和Observer的混合体,可以发送和接收数据。
private productInsertedSubject = new Subject<Product>();
// 将上面的Subject转换为Observable,以便外部可以订阅产品插入事件,但不能直接发布到它。
productInsertedAction$ = this.productInsertedSubject.asObservable();
// 使用merge操作符合并两个Observable流:一个是从API或其他源获取的产品类别流,另一个是产品插入动作流。
// 产品插入动作流使用scan操作符来累积插入的产品,形成一个产品数组。
productsWithAdd$ = merge(
this.productsWithCategory$,
this.productInsertedAction$
).pipe(
scan((acc: Product[], value: Product | Product[]) => {
if (Array.isArray(value)) {
// 如果 value 是 Product 类型的数组,将其元素添加到累加器中
return acc.concat(value);
} else if (value instanceof Product) {
// 如果 value 是单个 Product 实例,添加到累加器数组中
return acc.concat([value]);
} else {
// 如果 value 既不是数组也不是 Product 实例,抛出错误
throw new Error('Invalid product data');
}
}, [] as Product[]) // 初始值为空数组
);
// 使用combineLatest操作符组合两个Observable流:一个是包含已添加产品的流,另一个是类别选择动作流。
// 当这两个流中的任何一个发出新值时,都会触发这个组合流的更新。
products$ = combineLatest(
this.productsWithAdd$,
this.categorySelectedAction$
).pipe(
// 使用map操作符转换流中的值。这里,它根据所选的类别ID过滤产品。
map(([products, selectedCategoryId]) =>
products.filter(product =>
// 如果没有选择类别ID,则返回所有产品;否则,只返回与所选类别ID匹配的产品。
selectedCategoryId ? product.categoryId === selectedCategoryId : true
)
),
// 使用catchError操作符处理流中的错误。如果发生错误,它将错误消息分配给errorMessage属性,并返回一个不发出任何值的EMPTY Observable。
catchError(err => {
this.errorMessage = err;
return EMPTY;
})
);
// 定义一个方法,用于向productInsertedSubject发布新产品。它有一个默认参数,该参数是通过调用fakeProduct()方法生成的假产品对象。
addProduct(newProduct: Product = this.fakeProduct()) {
this.productInsertedSubject.next(newProduct);
}
// 定义一个Observable,它目前只发出一个空数组。在实际应用中,这应该被替换为从API或其他数据源获取的产品类别流。
productsWithCategory$ = of([]);
// 创建一个新的Subject实例,用于发布类别选择事件。这里的number类型应该是categoryId的类型。
categorySelectedAction$ = new Subject<number>();
// 定义一个方法,用于生成一个假的产品对象。这个方法应该返回一个符合Product接口的对象。
fakeProduct(): Product {
return { /* ... 你的产品对象结构 ... */ } as Product;
}
// 定义一个字符串属性,用于存储错误信息。
errorMessage: string;
}
// 定义一个接口,表示产品的结构。这个接口应该包含产品的所有属性,如id、name和categoryId等。
interface Product {
id: number;
name: string;
categoryId: number;
// ... 可以添加其他属性 ...
}
更进一步,可以采用发送请求但不请求回显的方式处理简单的 add 场景。
整理思路:
上述过程中,我们使用 merge + scan 的方式将新增数据和原来的数据结合起来;然后再使用 combineLatest + pipe + map + filter 的方式将目标类别过滤出来。最后得到的是 products$ 就可以在模板中通过 async 管道来使用了。
35. 缓存 observable
缓存的目的有两个,一个是为了提高性能,一个是为了减轻服务压力。
两种缓存方案:经典方案 和 声明式方案
private products: Product[] = [];
getProducts(): Observable<Product[]> {
if (this.products && this.products.length > 0) {
return of(this.products);
}
return this.http.get<Product[]>(this.url)
.pipe(
tap(data => this.products = data),
catchError(this.handleError)
);
}
private url = 'api/products';
private products$: Observable<Product[]>;
constructor(private http: HttpClient) {
this.products$ = this.http.get<Product[]>(this.url)
.pipe(
shareReplay(1), // 缓存数据,确保后续订阅者也能获取到相同的数据
catchError(this.handleError) // 捕获错误并处理
);
}
关于命令式和声明式这里多说一些:
命令式的代码相当于是白盒,看命令式的代码能够了解其中所有的实现细节,因此一般来说命令式的代码比较底层。而声明式的代码相当于是黑盒,看声明式的代码你只知道输入是什么输出是什么,但是从输入到输出的过程是不可见的,因此声明式编程中经常能够看到封装好的方法被调用。例如上述两个代码中,调用了 shareReplay(1) 之后就能缓存最近一次的数据,你知道这是可以实现的,但是不知道是怎么实现的。
对于一段代码,它可能既是命令式又是声明式的,这取决于你站的角度。站在业务的角度上述第一段代码是命令式的,但是站在 C++ 的角度,整个 JavaScript 都是声明式的。所以,立场不同,结果也不同。
与缓存相关的操作符有两个,下面分别介绍之。
35.1 shareReplay 操作符
shareReplay 是一个RxJS操作符,它允许多个订阅者共享同一个Observable,并重放定义数量的数据。使用shareReplay(1)可以缓存最新一个数据发射给新订阅者,适用于共享Observables、在应用中缓存数据以及为迟到的订阅者重放发射。
下面的图展示了 shareReplay 的作用原理:
shareReplay 是一个RxJS中的多播操作符,它返回一个Subject,该Subject共享对底层源Observable的单个订阅。它接受一个可选的缓冲区大小参数,表示缓存并重放的项目数量。订阅时,它会重放指定数量的发射项。这些项目会永久缓存,即使没有更多订阅者也依然保留。
使用代码进行验证,仔细研读下面的代码你就会明白 shareReply 的原理:
const { map, shareReplay, take } = require("rxjs/operators");
const { interval } = require("rxjs");
let subscription1, subscription2;
// 假设这是一个从API获取数据的Observable
const apiDataObservable = interval(100).pipe(
map(val => `Data point #${val}`),
// 模拟获取数据完成后的行为
take(5)
);
// 使用shareReplay(1)来共享和缓存数据
const sharedDataObservable = apiDataObservable.pipe(
shareReplay(1)
);
// 第一个订阅者
subscription1 = sharedDataObservable.subscribe(val => {
console.log('Subscription 1 received:', val);
});
// 等待2秒,让源Observable发出一些值
setTimeout(() => {
// 第二个订阅者
subscription2 = sharedDataObservable.subscribe(val => {
console.log('Subscription 2 received:', val);
});
}, 600);
// 取消订阅
setTimeout(() => {
subscription1.unsubscribe();
subscription2.unsubscribe();
}, 2000);
/**
* Subscription 1 received: Data point #0
* Subscription 1 received: Data point #1
* Subscription 1 received: Data point #2
* Subscription 1 received: Data point #3
* Subscription 1 received: Data point #4
* Subscription 2 received: Data point #4
*/
35.2 share 操作符
share 是RxJS中的另一个多播操作符,与 shareReplay 类似,但默认情况下不提供缓冲区,也不重放缓冲区中的数据。通过 share(config),可以设置一组配置选项来自定义行为。share(f) 允许通过函数 f 来创建一个自定义的多播连接,例如使用 ReplaySubject(1) 来缓存最新的一个发射项。此外,share 操作符提供了 resetOnComplete、resetOnError 和 resetOnRefCountZero 等配置选项,允许开发者控制当Observable完成、出错或订阅者数量为零时的行为。
35.3 使用操作符缓存数据
在服务中使用 shareReplay 缓存 HTTP Client 对象的返回值。
private productCategoriesUrl = 'api/productCategories';
private productCategories$: Observable<ProductCategory[]>;
constructor(private http: HttpClient) {
this.productCategories$ = this.http.get<ProductCategory[]>(this.productCategoriesUrl)
.pipe(
tap(data => console.log('categories', JSON.stringify(data))),
shareReplay(1), // 这里应该是英文逗号,而不是中文逗号
catchError(this.handleError) // 确保 handleError 方法已经定义
);
}
pipe 中的常客:
map
catchError
tap
shareReplay
缓存网络请求常用(见下):
pipe
switchMap
shareReplay
较为完整的例子:
import { combineLatest, Observable, of } from 'rxjs';
import { catchError, map, shareReplay, tap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Product } from './product'; // 确保 Product 类型已定义
@Injectable({
providedIn: 'root'
})
export class ProductService {
private productsUrl: string = 'api/products'; // 假定URL是这样定义的
products$: Observable<Product[]>;
productsWithCategory$: Observable<Product>;
constructor(private http: HttpClient, private productCategoryService: ProductCategoryService) { // 确保 ProductCategoryService 已定义
this.products$ = this.http.get<Product[]>(this.productsUrl)
.pipe(
tap(data => console.log('Products:', JSON.stringify(data))),
catchError(this.handleError) // 确保 handleError 方法已经定义
);
this.productsWithCategory$ = combineLatest(
this.products$,
this.productCategoryService.productCategories$ // 确保 productCategories$ 是 ProductCategoryService 的属性
).pipe(
map(([products, categories]) =>
products.map(product => ({
...product,
price: product.price ? product.price * 1.5 : 0,
category: categories.find(c => product.categoryId === c.id)?.name,
searchKey: [product.productName]
}) as Product)
),
shareReplay(1) // 确保 shareReplay 后面是英文分号
);
}
private handleError(error: any) {
// 错误处理逻辑
console.error('An error occurred:', error);
return of(error); // 返回一个Observable,这里只是示例,实际可能需要更复杂的错误处理
}
}
这段代码定义了一个 Angular 服务 ProductService,用于管理产品数据。下面是对代码的详细分析:
-
导入依赖:
- 从
rxjs导入了combineLatest,Observable,of等,这些是 RxJS 的核心组件,用于处理异步数据流。 - 从
rxjs/operators导入了catchError,map,shareReplay,tap等操作符,用于对 Observable 进行操作。 - 从
@angular/common/http导入HttpClient,用于发起 HTTP 请求。 - 从
@angular/core导入Injectable装饰器,用于创建服务。
- 从
-
@Injectable 装饰器:
- 标记
ProductService为可注入的服务,providedIn: 'root'表示这个服务是单例的,作为根模块的一部分提供。
- 标记
-
类定义:
ProductService类中定义了两个私有属性:productsUrl用于存储产品数据的 API 地址,products$和productsWithCategory$分别存储产品数据和包含类别的产品数据的 Observable。
-
构造函数:
- 接收
HttpClient和ProductCategoryService作为依赖注入,用于数据请求和获取产品类别数据。
- 接收
-
products$ 属性:
- 使用
this.http.get发起 GET 请求获取产品数据,并通过pipe链应用操作符。 tap操作符用于在数据流中打印日志,但不会改变数据流。catchError操作符用于处理请求过程中可能出现的错误,这里调用了handleError方法。
- 使用
-
productsWithCategory$ 属性:
- 使用
combineLatest操作符将products$和productCategoryService.productCategories$两个 Observable 合并,当任一 Observable 发出新的数据时,都会触发一个新的数据发射。 map操作符用于处理组合后的数据,创建一个新的产品数组,每个产品对象增加了价格(如果存在则乘以1.5),类别名称(通过查找匹配的类别ID),以及搜索关键字。shareReplay(1)操作符确保所有订阅者接收到最新的数据发射,并且数据会被缓存,即使后续有新的订阅者加入也能获取到之前的数据。
- 使用
-
handleError 方法:
- 私有方法,用于处理 Observable 中的错误。这里简单地打印错误并返回一个新的 Observable 发射错误对象,实际应用中可能需要更复杂的错误处理逻辑。
-
错误处理:
handleError方法中的of(error)是示例代码,通常错误处理不会这样简单。你可能需要根据错误类型进行不同的处理,或者返回一个特定的错误对象。
-
类型安全:
- 代码中使用了 TypeScript 的类型注解,例如
Product[]表示产品数组的类型,这有助于在开发过程中提供类型检查。
- 代码中使用了 TypeScript 的类型注解,例如
-
模块化:
- 通过导入
Product类型,代码保持了模块化,Product类型的定义应该在一个单独的文件中。
- 通过导入
35.4 在Angular应用中使用RxJS来管理数据刷新和缓存的策略
- 数据流动性和用户行为:考虑数据如何在应用中流动,以及用户如何与数据互动。
- 缓存控制:思考如何使缓存在一定时间后失效,或允许用户控制数据刷新的时机。
- 数据更新:在进行数据更新操作时,确保获取最新数据。
- BehaviorSubject用于刷新控制:使用
BehaviorSubject来触发数据刷新,当其发出新值时,会启动数据获取流程。 - 响应式编程:通过
pipe和mergeMap等操作符,将用户刷新请求或时间间隔的失效转换为HTTP请求,获取最新数据。 - 错误处理:使用
catchError操作符来处理可能发生的HTTP请求错误,并定义错误处理逻辑。
35.5 取消使用缓存
下面的代码展示了如何取消缓存,也就是重新发起请求获取数据。
import { Subject, BehaviorSubject } from 'rxjs';
import { switchMap, shareReplay } from 'rxjs/operators';
private reloadTrigger = new Subject<void>();
private dataObservable: Observable<any>;
constructor(private http: HttpClient) {
this.dataObservable = this.reloadTrigger.pipe(
startWith(null), // 触发初始加载
switchMap(() => this.fetchData()),
shareReplay(1)
);
}
fetchData(): Observable<any> {
// 执行数据获取逻辑,例如 HTTP 请求
}
getData(): Observable<any> {
return this.dataObservable;
}
reloadData() {
this.reloadTrigger.next(); // 触发数据重新加载
}
this.reloadTrigger.next(); 能够触发数据重新加载的原理:
Answer:
当调用 this.reloadTrigger.next() 时,这个操作会向 reloadTrigger 这个 Subject 发送一个新的值。由于 dataObservable 是基于 reloadTrigger 设置的,这个调用会触发一系列操作来更新 dataObservable 的值。以下是详细解释:
-
触发
reloadTrigger:this.reloadTrigger.next()触发reloadTrigger,向所有订阅了reloadTrigger的 Observable 发送一个新的值。 -
switchMap响应:dataObservable中的switchMap操作符正在监听reloadTrigger的值。当reloadTrigger发出新值时,switchMap会响应这个值。 -
取消前一个调用:
switchMap的行为是,一旦接收到新的输入值,它会取消前一个内部 Observable(如果有的话)的订阅。这意味着如果之前有正在进行的数据加载(例如,一个 HTTP 请求),它会被取消。 -
调用
fetchData:switchMap随后会调用fetchData()方法,这个方法应该返回一个新的 Observable,通常表示异步操作,如从服务器获取数据。 -
新的数据加载:
fetchData()返回的新的 Observable 会取代之前的 Observable,成为dataObservable的新的数据源。 -
更新订阅者: 所有订阅了
dataObservable的组件或其他服务将开始接收新的 Observable 发出的数据。如果fetchData()返回的是一个新的数据数组,那么这些数据将被发送给订阅者。 -
shareReplay(1)的作用:shareReplay(1)确保所有订阅者接收到相同的数据,并且缓存最新的一个值。如果新的订阅者在dataObservable完成之前订阅,它们将立即接收到缓存的最新值。 -
完成和错误处理: 如果
fetchData()返回的 Observable 完成或出错,switchMap将等待下一次reloadTrigger的触发来重新发起数据加载。
通过这种方式,this.reloadTrigger.next() 调用确保了 dataObservable 总是反映最新的数据加载结果,无论是初次加载还是后续的重新加载。这种模式在需要手动刷新或轮询的场景中非常有用。
总结: 使用下面的代码快速实现最后一次数据缓存的效果:
.pipe(
shareReplay(1), // 缓存数据,确保后续订阅者也能获取到相同的数据
catchError(this.handleError) // 捕获错误并处理
);
使用下面的模板实现具有缓存功能的网络请求服务:
import { Subject, BehaviorSubject } from 'rxjs';
import { switchMap, shareReplay } from 'rxjs/operators';
private reloadTrigger = new Subject<void>();
private dataObservable: Observable<any>;
constructor(private http: HttpClient) {
this.dataObservable = this.reloadTrigger.pipe(
startWith(null), // 触发初始加载
switchMap(() => this.fetchData()),
shareReplay(1)
);
}
fetchData(): Observable<any> {
// 执行数据获取逻辑,例如 HTTP 请求
}
getData(): Observable<any> {
return this.dataObservable;
}
reloadData() {
this.reloadTrigger.next(); // 触发数据重新加载
}
36. 高阶可观测对象
所谓高阶可观测对象可以用下图进行说明:
简单来说 observable 对象相互嵌套就会出现高阶可观测对象。而获取内层可观测对象包裹数据的朴素方法如下所示:
一般来说,不会使用内嵌的 observable, 而是使用一些特殊的操作符将其扁平化,这些常见的额操作符有:concatMap mergeMap switchMap. 高阶映射操作符是 RxJS 中的一类特殊操作符,它们将每个来自源(外部)Observable 的值映射到一个新的(内部)Observable。这些操作符会自动订阅和取消订阅内部 Observables,并将结果展平,最终将得到的值发射到输出 Observable。常见的高阶映射操作符包括mergeMap、switchMap、concatMap等。即:
- Replace nested subscriptions with higher-order mapping operators
使用高阶映射操作符可以将发射的项映射到一个新的Observable,并自动订阅和取消订阅该Observable,最后将结果发射到输出Observable。这些操作符接受一个输入项,并返回一个Observable,避免了嵌套订阅的问题。例如,使用map可能需要嵌套订阅,而switchMap或mergeMap可以直接在外部Observable的每个发射项上应用,简化了代码并管理了内部Observable的订阅生命周期。
37. concatMap 操作符
concatMap是RxJS中的一个高阶映射操作符,它将每个发射的项转换为一个新的内部Observable。与map不同,concatMap会等待每个内部Observable完成,然后才会处理下一个,确保结果以顺序的方式连接。 例如,使用concatMap(i => of(i)),它会按顺序发射每个项的值。concatMap 用来按顺序获取网络数据,因为它能保证上一个网络请求完成之后再发起下一个。
concatMap是 RxJS 中的一个转换操作符,它订阅输入 Observable 并创建输出 Observable。当输入 Observable 发射一个项目时,该项目被排队并映射为一个由提供函数指定的内部 Observable。concatMap订阅这个内部 Observable,并等待它完成。内部 Observable 的发射值被顺序连接到输出 Observable。只有当前一个内部 Observable 完成后,concatMap才会处理下一个项目,确保发射的值按顺序发出,避免了并发问题。这个操作符大多用在以下场景:
- From a set of ids, get data in sequence
- From a set of ids, update data in sequence
38. mergeMap 操作符
mergeMap 是RxJS中的高阶映射和合并操作符。它将每个发射的项转换为一个新的内部 Observable,如使用 mergeMap(i => of(i))。与 concatMap 不同,mergeMap 允许内部 Observables 并行执行,无论它们何时发射值,都会将这些值合并后发射到输出 Observable 中,从而可能改变原始数据发射的顺序。
mergeMap(之前称为flatMap)是 RxJS 中的转换操作符。它订阅输入 Observable,创建输出 Observable。当输入发射项目时,这些项目被映射为内部 Observable。mergeMap 订阅这些内部 Observable,并将它们的发射合并到输出 Observable 中,允许并行处理,可能改变发射顺序。
应用场景:
- To process in parallel
- When order doesn't matter
- From a set of ids, retrieve data (order doesn't matter)
39. switchMap 操作符
switchMap是RxJS中的高阶映射和切换操作符。它将输入Observable的每个发射项转换为新的内部Observable。无论之前是否有内部Observable在运行,每当新的发射项到来时,switchMap都会取消订阅之前的内部Observable,并切换到这个新的内部Observable。这意味着输出Observable始终只反映最新的内部Observable的发射值,忽略之前所有未完成的内部Observable。
也许下面这张图能更好的说明 switchMap 的特点:
switchMap 是 RxJS 中的转换操作符,它订阅输入 Observable 并创建输出 Observable。当输入发射项时,该项被映射为内部 Observable。switchMap 会取消订阅任何之前的内部 Observable,并切换到这个新的内部 Observable。这样,输出 Observable 只反映最新内部Observable的发射值,并将这些值合并到输出中。
应用场景: To stop any prior Observable before switching to the next one
- Type ahead or auto completion User selection from a list.
使用 switchMap 特别适合下面的需求:用户通过 input 输入用户名,根据用户名通过网络请求找到对应的 id 然后通过 id 找到对应的 ToDos 然后展示。因为 switchMap 可以看成是自带节流的! 但是!switchMap 只是不处理已经发出的网络请求,无法对网络请求本身节流,也就是说每当用户改变输入的时候还是会发出网络请求,只不过是不处理而已。如果要对网络请求本身节流,需要使用 debounce 操作符。
总结: concatMap、mergeMap和switchMap都是RxJS中处理Observable发射项与内部Observable关系的高阶映射操作符:
concatMap:顺序等待每个内部Observable完成,然后才开始处理下一个,保持发射项的原始顺序。mergeMap:允许内部Observable并行处理,发射的值将被合并,可能会改变发射项的顺序。switchMap:在新的发射项到来时,取消之前的内部Observable订阅,切换到新的内部Observable,只反映最新的Observable结果。正如下面的示例,使用 switchMap 并不能取消之前的网络请求,它确实会发出网络请求,但只是不对其进行处理而已!
todosForUserS = this.userEnteredActionS.pipe(
// Get the user given the username
switchMap((userName: string) =>
this.http.get<User>(`${this.userUrl}?username=${userName}`)
.pipe(
// Ensure we receive a user object
take(1),
// Get the todos given the user id
switchMap((user: User) =>
this.http.get<ToDo[]>(`${this.todoUrl}?userId=${user.id}`)
)
)
)
);
总结一下就是:concatMap 是用来根据 ids 顺序查数据的。mergeMap 使用根据 ids 乱序查数据的。而 switchMap 则是用来节流查数据的。
40. 两种数据更新的策略
这两种策略为:全量型和增量型方式。
- 全量的意思就是在刚开始的时候一次性拿到所有的数据然后再按需取用;
- 增量的意思是按需取用,不够的时候再从后端拿。
全量型示例代码
这种方式记得在服务类这一侧加上缓存:
增量型示例代码
注意这里的 toArray 方法。
上面的代码可以进行简化,实际上 from switchMap toArray 等价于 forkJoin.
上面的代码有两处需要注意:
- switchMap + from 不能被 map 替代;这是因为 switchMap 自带节流,而 map 没有节流的作用。
- 使用 filter(Boolean) 的方式快速过滤空值。
然后再加上放错处理和日志输出。
对比两种获取数据的方式
在处理相关数据流时,开发者面临两种不同的策略:获取全部数据(Get it All)和即时获取(Just in Time)。全量数据模式主要使用的是 combineLatest + map 而 增量数据模式主要用的是 filter + switchMap
-
获取全部数据(Get it All):
- 这是一种声明式模式,开发者声明需要从多个数据流中获取所有数据。主要会用到 combineLatest 操作符。
- 这种方法可以立即组合数据流并显示结果,因为它获取了所有必要的数据。
- 优点是简单直观,可以立即显示所有数据。
- 缺点是可能会导致不必要的数据加载,增加资源消耗。
-
即时获取(Just in Time, JIT):
- 这种方法使用高阶映射操作符,如
concatMap、mergeMap和switchMap,来根据需要检索数据。 - 它的优点是代码更复杂,但只检索所需的数据,从而减少资源消耗和提高效率。
- 缺点是可能会有显示延迟,因为数据的检索和处理是按需进行的。
- 这种方法使用高阶映射操作符,如
-
声明式模式:
- 通过声明式模式,开发者可以轻松地定义需要哪些数据,系统会自动处理数据的组合和显示。
- 这种方式适用于数据量不是很大的场景,或者数据更新不频繁的情况。
-
高阶映射操作符:
- 高阶映射操作符提供了更细粒度的控制,允许开发者根据具体的业务逻辑来决定如何以及何时获取和处理数据。
- 它们避免了嵌套订阅的问题,简化了异步数据流的处理。
总结来说,选择获取全部数据还是即时获取,取决于具体的应用场景和需求。获取全部数据适用于需要快速显示所有数据的情况,而即时获取则适用于需要按需加载数据以提高性能和效率的情况。高阶映射操作符为开发者提供了强大的工具来实现复杂的数据流处理逻辑。
41. async 管道优化 -- 统一的数据入口
如下图所示,我们不想给每个单独的数据都写一个 async 管道及指定别名,我们希望有一个统一的数据入口:
这样我们只需要在组件的最外层引入一次即可:
注意上图中的技巧,真的很惊艳:
<div class="card" *ngIf="vm$ | async as vm">...</div>
注意这里的数据总口是开在 *ngIf 这个指令中的。
我们可以通过下面的方式实现这种组件数据接口的统一。
vmS = combineLatest([
this.productS,
this.productSuppliersS, // 假设存在productSuppliersS Observable
this.pageTitleS
]).pipe(
map(([product, productSuppliers, pageTitle]) => {
// 这里可以根据需要处理或返回数据
return { product, productSuppliers, pageTitle };
})
);
// 假设在组件的HTML模板中使用
<div *ngIf="vmS | async as m">
<!-- 使用m.product, m.productSuppliers, m.pageTitle -->
</div>
关于 as 多说一些:
<div *ngIf="productsSasync as products">
<table>
<tr *ngFor="let product of products">
<td>{{ product.productName </td>
<td>ff product.productCode }}</td>
</tr>
</table>
</div>
使用 as 可以定义新的模板变量!
简化 async 管道的意义在于:可以减少大量的订阅、取消订阅代码以及相关的逻辑;减少内存泄漏的风险!
总结: *ngIf + | async + combineLatest + map 可实现数据的统一入口处理。
42. 数据流和事件流
数据流
数据流(Data Streams)是响应式编程中的基本概念,通常用于表示异步数据的流动。一个数据流可以发射单个项目,例如一个响应,然后完成。这个响应经常是一个数组,包含了需要进一步处理的数据项。
要转换数组中的元素,可以使用映射(Map)操作。映射操作符允许对发射的每个数组元素应用一个函数,从而实现数据的转换。例如,如果你有一个发射产品数组的Observable,你可以使用map操作符来提取或修改每个产品的特定属性。
这个过程通常涉及以下几个步骤:
- 监听数据流,等待其发射数组。
- 使用
map操作符处理发射的数据。 - 对数组中的每个元素应用一个转换函数。
- 将转换后的元素组成新的数组,或者直接处理转换后的每个元素。
通过这种方式,数据流不仅可以发射数据,还可以在数据发射时立即进行数据的转换和处理,从而实现高效的数据操作和响应式UI更新。
事件流
动作流(Action Streams)是响应式编程中的一个概念,它代表了一个动作或事件的序列,例如用户点击按钮。动作流的特点在于它们仅在被激活时发射数据,一旦流被停止,就不会再发射任何数据。这与普通的数据流不同,后者可能会持续发射数据,直到它们自然完成或被外部条件终止。
在动作流中,一个常见的问题是错误处理。如果动作流中出现未处理的错误,流将自动停止,不再发射任何数据。为了防止这种情况导致整个流的中断,最佳实践是使用错误处理操作符,比如catchError,来捕获这些错误。
使用catchError时,不应该简单地用EMPTY来替换发生错误的Observable,因为这样会停止整个流(这是因为 EMPTY 会立即触发 complete 回调停止整个流)。相反,应该用一个默认值或特定的空值来替换它,这样可以保持流的活跃状态,同时优雅地处理错误情况。例如,如果一个表单提交的动作流因为后端服务不可用而失败,可以返回一个默认的错误消息或状态,而不是完全停止用户的操作体验。
通过这种方式,动作流可以更加健壮地处理错误,同时保持用户界面的响应性和交互性。这种模式在构建用户界面和处理用户交互时尤为重要,因为它可以确保即使在部分功能失败的情况下,应用仍然可以继续运行,提供反馈给用户。
43. 冷/热 可观察对象
由 Subject 等创建的可观察对象是 热 可观察对象,它不在意是否有人订阅了它。
组合操作符和创建函数如 combineLatest 和 forkJoin ,要求每个输入 Observable 至少发射一次数据后才发射。combineLatest 在最新值到来时发射,forkJoin 则等待所有输入完成。与动作流结合时,使用 BehaviorSubject 因为它能提供默认值并立即发射,而不是等待首次发射。
45. 一些小技巧
不要写过长的 pipe
调试数据类型
- ls there a subscription?
- Is there an operator waiting for completion?
- Start at the source (HTTP request)
- Walk through each operator in the pipeline
- Follow through to the Ul
画图分析
你应该掌握的内容
45. RxJS 基础知识
45.1 概述
45.1.1 什么是 RxJS ?
RxJS 是一个用于处理异步编程的 JavaScript 库,目标是使编写异步和基于回调的代码更容易。
45.1.2 为什么要学习 RxJS ?
就像 Angular 深度集成 TypeScript 一样,Angular 也深度集成了 RxJS。
服务、表单、事件、全局状态管理、异步请求 ...
45.1.3 快速入门
-
可观察对象 ( Observable ) :类比 Promise 对象,内部可以用于执行异步代码,通过调用内部提供的方法将异步代码执行的结果传递到可观察对象外部。
-
观察者 ( Observer ):类比 then 方法中的回调函数,用于接收可观察对象中传递出来数据。
-
订阅 ( subscribe ):类比 then 方法,通过订阅将可观察对象和观察者连接起来,当可观察对象发出数据时,订阅者可以接收到数据。
import { Observable } from 'rxjs';
const observable = new Observable(function(observer) {
setTimeout(function() {
observer.next({
name: '张三'
});
}, 2000);
});
const observer = {
next: function(value) {
console.log(value);
}
};
observable.subscribe(observer);
45.2 可观察对象
45.2.1 Observable
- 在 Observable 对象内部可以多次调用 next 方法向外发送数据。
import { Observable } from 'rxjs';
const observable = new Observable(function(observer) {
let index = 0;
setInterval(function() {
observer.next(index++);
}, 1000);
});
const observer = {
next: function(value) {
console.log(value);
}
};
observable.subscribe(observer);
- 当所有数据发送完成以后,可以调用 complete 方法终止数据发送。
import { Observable } from 'rxjs';
const observable = new Observable(function(observer) {
let index = 0;
let timer = setInterval(function() {
observer.next(index++);
if (index === 3) {
observer.complete();
clearInterval(timer);
}
}, 1000);
});
const observer = {
next: function(value) {
console.log(value);
},
complete: function() {
console.log("数据发送完成");
}
};
observable.subscribe(observer);
- 当 Observable 内部逻辑发生错误时,可以调用 error 方法将失败信息发送给订阅者,Observable 终止。
import { Observable } from 'rxjs';
const observable = new Observable(function(observer) {
let index = 0;
let timer = setInterval(function() {
observer.next(index++);
if (index === 3) {
observer.error('发生错误');
clearInterval(timer);
}
}, 1000);
});
const observer = {
next: function(value) {
console.log(value);
},
error: function(error) {
console.log(error);
}
};
observable.subscribe(observer);
- 可观察对象是惰性的,只有被订阅后才会执行
const observable = new Observable(function () {
console.log("Hello RxJS")
})
// observable.subscribe()
- 可观察对象可以有 n 多订阅者,每次被订阅时都会得到执行
const observable = new Observable(function () {
console.log("Hello RxJS")
})
observable.subscribe()
observable.subscribe()
observable.subscribe()
observable.subscribe()
observable.subscribe()
- 取消订阅
import { interval } from "rxjs"
const obs = interval(1000)
const subscription = obs.subscribe(console.log)
setTimeout(function () {
subscription.unsubscribe()
}, 2000)
45.2.2 Subject
- 用于创建空的可观察对象,在订阅后不会立即执行,next 方法可以在可观察对象外部调用
import { Subject } from "rxjs"
const demoSubject = new Subject()
demoSubject.subscribe({next: function (value) {console.log(value)}})
demoSubject.subscribe({next: function (value) {console.log(value)}})
setTimeout(function () {
demoSubject.next("hahaha")
}, 3000)
45.2.3 BehaviorSubject
拥有 Subject 全部功能,但是在创建 Obervable 对象时可以传入默认值,观察者订阅后可以直接拿到默认值。
import { BehaviorSubject } from "rxjs"
const demoBehavior = new BehaviorSubject("默认值")
demoBehavior.subscribe({next: function (value) {console.log(value)}})
demoBehavior.next("Hello")
45.2.3 ReplaySubject
功能类似 Subject,但有新订阅者时两者处理方式不同,Subject 不会广播历史结果,而 ReplaySubject 会广播所有历史结果。
import { ReplaySubject } from 'rxjs';
const rSubject = new ReplaySubject();
rSubject.subscribe(value => {
console.log(value);
});
rSubject.next("Hello 1");
rSubject.next("Hello 2");
setTimeout(function() {
rSubject.subscribe({
next: function(value) {
console.log(value);
}
});
}, 3000);
45.3 辅助方法
45.3.1 range
range(start, length),调用方法后返回 observable 对象,被订阅后会发出指定范围的数值。
import { range } from "rxjs"
range(0, 5).subscribe(n => console.log(n))
// 0
// 1
// 2
// 3
// 4
方法内部并不是一次发出 length 个数值,而是发送了 length 次,每次发送一个数值,就是说内部调用了 length 次 next 方法。
45.3.2 of
将参数列表作为数据流返回。
of("", "b", [], {}, true, 20).subscribe(v => console.log(v))
45.3.3 from
将 Array,Promise, Iterator 转换为 observable 对象。
from(["a", "b", "c"]).subscribe(v => console.log(v))
// a
// b
// c
import { from } from 'rxjs';
function p() {
return new Promise(function(resolve) {
resolve([100, 200]);
});
}
from(p()).subscribe(v => console.log(v));
// Expected output: [100, 200]
45.3.4 interval、timer
Interval: 每隔一段时间发出一个数值,数值递增
import { interval } from "rxjs"
interval(1000).subscribe(n => console.log(n))
timer: 间隔时间过去以后发出数值,行为终止,或间隔时间发出数值后,继续按第二个参数的时间间隔继续发出值
import { timer } from "rxjs"
timer(2000).subscribe(n => console.log(n))
timer(0, 1000).subscribe(n => console.log(n))
以下是使用 timer 函数的两个例子,第一个例子演示了在指定的延迟之后发出单个数值并终止,第二个例子演示了在指定的延迟之后开始,并按照指定的时间间隔连续发出数值:
import { timer } from 'rxjs';
// 例子 1: 发出一个数值后终止
timer(3000).subscribe({
next: value => console.log(`单个数值发出: ${value}`), // 这将只被调用一次
complete: () => console.log('定时器完成')
});
// 这将在 3 秒后打印 "单个数值发出: 0" 然后 "定时器完成"
// 例子 2: 以指定的时间间隔连续发出数值
timer(1000, 2000).subscribe({
next: value => console.log(`连续数值发出: ${value}`),
// 这将每 2 秒被调用一次,从 0 开始递增
});
在第一个例子中,timer(3000) 会在 3 秒后发出一个值(在这个例子中是 0),然后完成。在第二个例子中,timer(1000, 2000) 会在 1 秒后开始发出值,并在随后每 2 秒发出一个递增的值(0, 1, 2, ...),这些值会一直发出,直到你手动取消订阅或者达到其他终止条件。
45.3.5 concat
合并数据流,先让第一个数据流发出值,结束后再让第二个数据流发出值,进行整体合并。
import { concat, range } from "rxjs"
concat(range(1, 5), range(6, 5)).subscribe(console.log)
45.3.6 merge
合并数据流,多个参数一起发出数据流,按照时间线进行交叉合并。
import { merge, fromEvent, interval } from "rxjs"
const clicks = fromEvent(document, "click")
const timer = interval(1000)
merge(clicks, timer).subscribe(console.log)
45.3.7 combineLatest
将两个 Obserable 中最新发出的数据流进行组合成新的数据流,以数组的形式发出。和当前最新的进行组合。
import { combineLatest, timer } from "rxjs"
const firstTimer = timer(0, 1000) // emit 0, 1, 2... after every second, starting from now
const secondTimer = timer(500, 1000) // emit 0, 1, 2... after every second, starting 0,5s from now
combineLatest(firstTimer, secondTimer).subscribe(console.log)
// [0, 0] after 0.5s
// [1, 0] after 1s
// [1, 1] after 1.5s
// [2, 1] after 2s
45.3.8 zip
将多个 Observable 中的数据流进行组合。和将来最新的进行组合。
import { zip, of } from "rxjs"
import { map } from "rxjs/operators"
let age = of(27, 25, 29)
let name = of("Foo", "Bar", "Beer")
let isDev = of(true, true, false)
zip(name, age, isDev)
.pipe(map(([name, age, isDev]) => ({ name, age, isDev })))
.subscribe(console.log)
// { name: 'Foo', age: 27, isDev: true }
// { name: 'Bar', age: 25, isDev: true }
// { name: 'Beer', age: 29, isDev: false }
45.3.9 forkJoin
forkJoin 是 Rx 版本的 Promise.all(),即表示等到所有的 Observable 都完成后,才一次性返回值。
import axios from "axios"
import { forkJoin, from } from "rxjs"
axios.interceptors.response.use(response => response.data)
forkJoin({
goods: from(axios.get("http://localhost:3005/goods")),
category: from(axios.get("http://localhost:3005/category")),
}).subscribe(console.log)
45.3.10 throwError
返回可观察对象并向订阅者抛出错误。
import { throwError } from "rxjs"
throwError("发生了未知错误").subscribe({ error: console.log })
45.3.11 retry
如果 Observable 对象抛出错误,则该辅助方法会重新订阅 Observable 以获取数据流,参数为重新订阅次数。
import { interval, of, throwError } from 'rxjs';
import { mergeMap, retry } from 'rxjs/operators';
interval(1000).pipe(
mergeMap(val => {
if (val > 2) {
return throwError('Error!');
}
return of(val);
}),
retry(2)
).subscribe({
next: console.log,
error: console.log
});
45.3.12 race
接收并同时执行多个可观察对象,只将最快发出的数据流传递给订阅者。
import { race, timer } from "rxjs"
import { mapTo } from "rxjs/operators"
const obs1 = timer(1000).pipe(mapTo("fast one"))
const obs2 = timer(3000).pipe(mapTo("medium one"))
const obs3 = timer(5000).pipe(mapTo("slow one"))
race(obs3, obs1, obs2).subscribe(console.log)
13.3.13 fromEvent
将事件转换为 Observable。
import { fromEvent } from "rxjs"
const btn = document.getElementById("btn")
// 可以将 Observer 简写成一个函数,表示 next
fromEvent(btn, "click").subscribe(e => console.log(e))
45.4 操作符
-
数据流:从可观察对象内部输出的数据就是数据流,可观察对象内部可以向外部源源不断的输出数据。
-
操作符:用于操作数据流,可以对象数据流进行转换,过滤等等操作。
45.4.1 map、mapTo
map: 对数据流进行转换,基于原有值进行转换。
import { interval } from "rxjs"
import { map } from "rxjs/operators"
interval(1000)
.pipe(map(n => n * 2))
.subscribe(n => console.log(n))
mapTo: 对数据流进行转换,不关心原有值,可以直接传入要转换后的值。
import { interval } from "rxjs"
import { mapTo } from "rxjs/operators"
interval(1000).pipe(mapTo({ msg: "接收到了数据流" })).subscribe(msg => console.log(msg))
45.4.2 filter
对数据流进行过滤。
import { range } from "rxjs"
import { filter } from "rxjs/operators"
range(1, 10).pipe(filter(n => n % 2 === 0)).subscribe(even => console.log(even))
45.4.3 pluck
获取数据流对象中的属性值。
import { interval } from "rxjs"
import { pluck, mapTo } from "rxjs/operators"
interval(1000).pipe(mapTo({ name: "张三", a: { b: "c" } }),pluck("a", "b"))
.subscribe(n => console.log(n))
45.4.4 first
获取数据流中的第一个值或者查找数据流中第一个符合条件的值,类似数组中的 find 方法。获取到值以后终止行为。
import { interval } from 'rxjs';
import { first } from 'rxjs/operators';
interval(1000)
.pipe(
first()
)
.subscribe(n => console.log(n));
interval(1000)
.pipe(
first(n => n === 3)
)
.subscribe(n => console.log(n));
45.4.5 startWith
创建一个新的 observable 对象并将参数值发送出去,然后再发送源 observable 对象发出的值。
在异步编程中提供默认值的时候非常有用。
import { interval } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
interval(1000)
.pipe(
map(n => n + 100),
startWith(505)
)
.subscribe(n => console.log(n));
// Expected output with timing (assuming no unsubscription):
// 505 (immediate due to startWith)
// 100 (after 1 second)
// 101 (after 2 seconds)
// 102 (after 3 seconds)
// ... and so on, each subsequent number being 100 more than the previous, with new numbers emitted every second
45.4.6 every
查看数据流中的每个值是否都符合条件,返回布尔值。类似数组中的 every 方法。
import { range } from 'rxjs';
import { every, map } from 'rxjs/operators';
range(1, 9)
.pipe(
map(n => n * 2),
every(n => n % 2 === 0)
)
.subscribe(b => console.log(b));
45.4.7 delay、delayWhen
delay: 对上一环节的操作整体进行延迟,只执行一次。
import { from } from 'rxjs';
import { delay, map, tap } from 'rxjs/operators';
from([1, 2, 3])
.pipe(
delay(1000),
tap(n => console.log("已经延迟 1s", n)),
map(n => n * 2),
delay(1000),
tap(() => console.log("又延迟了 1s"))
)
.subscribe(console.log);
// Expected output with timing:
// "已经延迟 1s" 1 (after 1 second)
// "已经延迟 1s" 2 (after 2 seconds)
// "已经延迟 1s" 3 (after 3 seconds)
// 2 (after 4 seconds)
// 4 (after 5 seconds)
// 6 (after 6 seconds)
// "又延迟了 1s" (after 7 seconds)
delayWhen: 对上一环节的操作进行延迟,上一环节发出多少数据流,传入的回调函数就会执行多次。
import { range, timer } from 'rxjs';
import { delayWhen } from 'rxjs/operators';
range(1, 10)
.pipe(
delayWhen(n => {
console.log(n);
return timer(n * 1000);
})
)
.subscribe(console.log);
45.4.8 take、takeWhile、takeUtil
take:获取数据流中的前几个
import { range } from "rxjs"
import { take } from "rxjs/operators"
range(1, 10).pipe(take(5)).subscribe(console.log)
takeWhile: 根据条件从数据源前面开始获取。
import { range } from "rxjs"
import { takeWhile } from "rxjs/operators"
range(1, 10)
.pipe(takeWhile(n => n < 8))
.subscribe(console.log)
takeUntil: 接收可观察对象,当可观察对象发出值时,终止主数据源。
import { interval, timer } from "rxjs"
import { takeUntil } from "rxjs/operators"
interval(100)
.pipe(takeUntil(timer(2000)))
.subscribe(console.log)
// 结果少两个数据流的原因:第一次和最后一次,都需要延迟 100 毫秒。
45.4.9 skip、skipWhile、skipUntil
**skip:**跳过前几个数据流。
import { range } from "rxjs"
import { skip } from "rxjs/operators"
range(1, 10).pipe(skip(5)).subscribe(console.log)
**skipWhile:**根据条件进行数据流的跳过。
import { range } from "rxjs"
import { skipWhile } from "rxjs/operators"
range(1, 10)
.pipe(skipWhile(n => n < 5))
.subscribe(console.log)
skipUntil: 跳过数据源中前多少时间发出的数据流,发送从这个时间以后数据源中发送的数据流。
import { timer, interval } from "rxjs"
import { skipUntil } from "rxjs/operators"
interval(100)
.pipe(skipUntil(timer(2000)))
.subscribe(console.log)
45.4.10 last
获取数据流中的最后一个。
import { range } from "rxjs"
import { last } from "rxjs/operators"
range(1, 10).pipe(last()).subscribe(console.log)
如果数据源不变成完成状态,则没有最后一个。
import { interval } from "rxjs"
import { last, take } from "rxjs/operators"
interval(1000).pipe(take(5), last()).subscribe(console.log)
45.4.11 concatAll、concatMap
concatAll: 有时 Observable 发出的又是一个 Obervable,concatAll 的作用就是将新的可观察对象和数据源进行合并。
Observable => [1, 2, 3]
Observable => [Observable, Observable]
import { fromEvent, interval } from "rxjs"
import { map, take, concatAll } from "rxjs/operators"
fromEvent(document, "click")
.pipe(
map(event => interval(1000).pipe(take(2))),
concatAll()
)
.subscribe(console.log)
import { map, concatAll } from "rxjs/operators"
import { of, interval } from "rxjs"
interval(1000)
.pipe(
map(val => of(val + 10)),
concatAll()
)
.subscribe(console.log)
**concatMap:**合并可观察对象并处理其发出的数据流。
45.4.13 reduce、scan
reduce: 类似 JavaScript 数组中的 reduce,对数数据进行累计操作。reduce 会等待数据源中的数据流发送完成后再执行,执行时 reduce 内部遍历每一个数据流进行累计操作,操作完成得到结果将结果作为数据流发出。
import { interval } from "rxjs"
import { take, reduce } from "rxjs/operators"
interval(500).pipe(
take(5),
reduce((acc, value) => acc += value, 0)
)
.subscribe(v => console.log())
scan:类似 reduce,进行累计操作,但执行时机不同,数据源每次发出数据流 scan 都会执行。reduce 是发送出最终计算的结果,而 scan 是发出每次计算的结果。
import { interval } from "rxjs"
import { take, scan } from "rxjs/operators"
interval(500)
.pipe(
take(5),
scan((acc, value) => acc += value, 0),
)
.subscribe(v => console.log())
45.4.14 mergeAll、mergeMap
**mergeAll:**交叉合并可观察对象。
import { fromEvent, interval } from "rxjs"
import { map, mergeAll } from "rxjs/operators"
fromEvent(document, "click")
.pipe(
map(() => interval(1000)),
mergeAll()
)
.subscribe(console.log)
mergeMap:交叉合并可观察对象以后对可观察对象发出的数据流进行转换。
import { of, interval } from "rxjs"
import { mergeMap, map } from "rxjs/operators"
of("a", "b", "c")
.pipe(mergeMap(x => interval(1000).pipe(map(i => x + i))))
.subscribe(x => console.log(x))
45.4.15 throttleTime
节流,可观察对象高频次向外部发出数据流,通过 throttleTime 限制在规定时间内每次只向订阅者传递一次数据流。
import { fromEvent } from "rxjs"
import { throttleTime } from "rxjs/operators"
fromEvent(document, "click")
.pipe(throttleTime(2000))
.subscribe(x => console.log(x))
45.4.16 debounceTime
防抖,触发高频事件,只响应最后一次。
import { fromEvent } from "rxjs"
import { debounceTime } from "rxjs/operators"
fromEvent(document, "click")
.pipe(debounceTime(1000))
.subscribe(x => console.log(x))
45.4.17 distinctUntilChanged
检测数据源当前发出的数据流是否和上次发出的相同,如相同,跳过,不相同,发出。
import { of } from "rxjs"
import { distinctUntilChanged } from "rxjs/operators"
of(1, 1, 2, 2, 2, 1, 1, 2, 3, 3, 4)
.pipe(distinctUntilChanged())
.subscribe(x => console.log(x)) // 1, 2, 1, 2, 3, 4
45.4.18 groupBy
对数据流进行分组。
import { of } from 'rxjs';
import { mergeMap, groupBy, toArray } from 'rxjs/operators';
of(
{ name: "Sue", age: 25 },
{ name: "Joe", age: 30 },
{ name: "Frank", age: 25 },
{ name: "Sarah", age: 35 }
)
.pipe(
groupBy(person => person.age),
mergeMap(group => group.pipe(toArray()))
)
.subscribe(console.log);
// Expected output:
// [{name: "Sue", age: 25}, { name: "Frank", age: 25 }]
// [{ name: "Joe", age: 30 }]
// [{ name: "Sarah", age: 35 }]
45.4.19 withLatestFrom
主数据源发出的数据流总是和支数据源中的最新数据流进行结合,返回数组。
import { fromEvent, interval } from "rxjs"
import { withLatestFrom } from "rxjs/operators"
const clicks = fromEvent(document, "click")
const timer = interval(1000)
clicks.pipe(withLatestFrom(timer)).subscribe(console.log)
45.4.20 switchMap
切换可观察对象。
import { fromEvent, interval } from "rxjs"
import { switchMap } from "rxjs/operators"
fromEvent(document, "click")
.pipe(switchMap(ev => interval(1000)))
.subscribe(x => console.log(x))
45.5 练习
45.5.1 元素拖拽
<style>
#box {
width: 200px;
height: 200px;
background: skyblue;
position: absolute;
left: 0;
top: 0;
}
</style>
<div id="box"></div>
// 原生 JavaScript
const box = document.getElementById('box');
box.onmousedown = function(event) {
let distanceX = event.clientX - box.offsetLeft;
let distanceY = event.clientY - box.offsetTop;
document.onmousemove = function(event) {
let positionX = event.clientX - distanceX;
let positionY = event.clientY - distanceY;
box.style.left = positionX + 'px';
box.style.top = positionY + 'px';
};
box.onmouseup = function() {
document.onmousemove = null;
};
};
// RxJS
import { fromEvent } from 'rxjs';
import { map, switchMap, takeUntil } from 'rxjs/operators';
const box = document.getElementById('box');
fromEvent(box, 'mousedown')
.pipe(
map(event => ({
distanceX: event.clientX - box.offsetLeft,
distanceY: event.clientY - box.offsetTop
})),
switchMap(({ distanceX, distanceY }) =>
fromEvent(document, 'mousemove').pipe(
map(event => ({
positionX: event.clientX - distanceX,
positionY: event.clientY - distanceY
})),
takeUntil(fromEvent(document, 'mouseup'))
)
)
)
.subscribe(({ positionX, positionY }) => {
box.style.left = `${positionX}px`;
box.style.top = `${positionY}px`;
});
45.5.2 搜索
<input id="search" type="text" placeholder="请输入搜索内容..." />
import { fromEvent, from, throwError } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap, catchError } from 'rxjs/operators';
import axios from 'axios';
const search = document.getElementById('search');
fromEvent(search, 'keyup')
.pipe(
debounceTime(700),
map(event => event.target.value),
distinctUntilChanged(),
switchMap(keyword =>
from(
axios.get(`https://j1sonplaceholder.typicode.com/posts?q=${keyword}`)
).pipe(
map(response => response.data),
catchError(error => throwError(`发生了错误: ${error.message}`))
)
)
)
.subscribe({
next: value => {
console.log(value);
},
error: error => {
console.log(error);
}
});
上述 JavaScript 代码段使用了 RxJS 库来处理输入字段的搜索功能。它监听输入字段的 keyup 事件,然后在每次按键事件后等待 700 毫秒(这有助于减少对服务器的请求次数)。如果用户输入的值与前一个值不同,它将触发一个请求到指定的 API 端点,获取搜索结果,并打印这些结果到控制台。如果在请求过程中发生错误,它将捕获错误并打印出错误消息。
45.5.3 串联请求
先获取token,再根据token获取用户信息
<button id="btn">获取用户信息</button>
import axios from "axios"
import { from, fromEvent } from "rxjs"
import { pluck, concatMap } from "rxjs/operators"
const button = document.getElementById("btn")
fromEvent(button, "click")
.pipe(
concatMap(event =>
from(axios.get("http://localhost:3005/token")).pipe(pluck("data", "token"))
),
concatMap(token =>
from(axios.get("http://localhost:3005/userInfo")).pipe(pluck("data"))
))
.subscribe(console.log)
实践与练习
1. 使用 combineLatest 在实际项目中隐藏打印日志的功能
@ViewChild("listTitle") listTitle: ElementRef;
ngAfterViewInit() {
this.clickFlow$ = fromEvent(this.listTitle.nativeElement, 'click');
// this.metaDate$ = interval(100).pipe(mapTo(() => ({
// origin: window.location.origin,
// time: new Date().toLocaleTimeString(),
// })));
this.metaDate$ = of({
origin: window.location.origin,
date: new Date().toDateString(),
});
combineLatest([this.metaDate$, this.clickFlow$]).pipe(
takeUntil(this.isComponentDestroyed),
map(([$1, $2]) => {
const { origin, date } = $1;
const { target } = $2;
const time = new Date().toLocaleTimeString();
return { origin, date, time, text: target?.innerText };
}),
catchError(error => {
return throwError({ error });
}),
tap(data => {
console.log(this.print.prefix, this.print.style, data)
})
).subscribe()
}
基本原理
这段代码首先通过@ViewChild装饰器获取模板中DOM元素的引用,然后创建两个Observable:一个是监听该DOM元素点击事件的Observable,另一个是发出包含当前页面源和日期的对象的Observable。这两个Observable通过combineLatest操作符合并,每当任一Observable发出新值时,都会组合最新的值进行处理。处理流程包括映射数据、捕获错误、打印日志,并通过订阅来启动数据流。
逐行分析
@ViewChild("listTitle") listTitle: ElementRef;- 使用
@ViewChild装饰器获取模板中带有#listTitle模板引用变量的DOM元素的引用,并将其存储在listTitle变量中。
- 使用
ngAfterViewInit() {- Angular生命周期钩子,表示视图已完全初始化,此时可以安全地访问视图中的DOM元素。
this.clickFlow$ = fromEvent(this.listTitle.nativeElement, 'click');- 使用
fromEvent函数创建一个Observable,它监听listTitle元素上的点击事件。
- 使用
this.metaDate$ = of({ origin: window.location.origin, date: new Date().toDateString(), });- 使用
of函数创建一个Observable,它发出一个包含当前页面源和当前日期的对象。这个Observable只发出一次值,然后完成。
- 使用
combineLatest([this.metaDate$, this.clickFlow$]).pipe(- 使用
combineLatest函数将metaDate$和clickFlow$这两个Observable合并。只有当两个Observable都发出至少一个值时,才会发出组合后的值。
- 使用
takeUntil(this.isComponentDestroyed),- 使用
takeUntil操作符来自动取消订阅,防止内存泄漏。当isComponentDestroyed发出值时,数据流将完成。
- 使用
map(([$1, $2]) => { ... }),- 使用
map操作符来处理组合后的值。它解构两个源Observable发出的值,并返回一个新的对象,该对象包含页面源、日期、时间和点击元素的文本内容。
- 使用
catchError(error => { return throwError({ error }); }),- 使用
catchError操作符来捕获数据流中的错误,并将错误对象包装在一个新的Observable中抛出。
- 使用
tap(data => { console.log(this.print.prefix, this.print.style, data) })- 使用
tap操作符来执行副作用,这里是在控制台打印出处理后的数据,以及可能的前缀和样式。
- 使用
).subscribe()- 订阅Observable,启动数据流。由于
subscribe方法被调用,数据流开始流动。
- 订阅Observable,启动数据流。由于
整体来看,这段代码展示了如何在Angular组件中使用RxJS来处理DOM事件和数据流,包括创建Observable、合并Observable、处理数据、捕获错误和打印日志。
2. 使用 forkJoin + resolver 完成路由跳转前所有数据的准备
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, forkJoin, of } from 'rxjs';
import { AlarmDataService } from '../services/alarm-data.service';
@Injectable({
providedIn: 'root',
})
export class AlarmResolverService implements Resolve<any> {
constructor(private alarmDataService: AlarmDataService) { }
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> | Promise<any> | any {
// 调用服务方法获取数据
try {
const id = route.params.id;
if (!id || id == '-1') throw 'Error';
const statusOptions$ = this.alarmDataService.statusOptions;
const alarmData$ = this.alarmDataService.getAlarmData(id);
// return this.alarmDataService.statusOptions;
return forkJoin([statusOptions$, alarmData$])
} catch (error) {
return forkJoin([of([]), of({})]);
}
}
}
其中 alarmDataService 服务为:
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { IAlarmDetails, SelectOption } from '../iotAlarmDetails/alarm-detail.component';
import { Injectable } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { IAlarm } from '@c8y/client';
import { catchError, map, tap } from 'rxjs/operators';
import { AlarmTrackerError } from '../errors/AlarmTrackerError';
interface IAlarmDetailsData {
code: number;
data: IAlarmDetails;
message: string;
success: boolean;
}
@Injectable({
providedIn: 'root'
})
export class AlarmDataService {
private current_data = null;
constructor(private http: HttpClient) { }
getAlarmData(id: string): Observable<IAlarmDetails> {
if (!id || id == '-1') return of({} as IAlarmDetails);
return this.http.post<IAlarmDetailsData>('http://localhost:3333/api/alarmData', { id }).pipe(
tap(data => console.log(data)),
map((data: IAlarmDetailsData) => {
const { success, data: alarm } = data;
if (success) {
return alarm;
} else {
throw 'Error';
}
}),
catchError(err => of({} as IAlarmDetails)
))
}
getStatusOptions(): Observable<SelectOption[]> {
return this.http.get<SelectOption[]>('http://localhost:3333/api/deviceStatusOption').pipe(
tap(data => console.log(data)),
catchError(err => of([
{ value: '', label: 'None$', },
{ value: 'Running', label: 'Running$', },
{ value: 'Offline', label: 'Offline$', },
])
))
}
getAllAlarms(): Observable<IAlarm[] | AlarmTrackerError> {
return this.http.get<IAlarm[]>('/api/alarms').pipe(
catchError((err: HttpErrorResponse) => this.handleHttpError(err))
)
}
private handleHttpError(err: HttpErrorResponse) {
let dataError = new AlarmTrackerError();
dataError.code = 10;
dataError.message = err.statusText;
dataError.friendlyMessage = 'An error occured retrieving data';
return throwError(dataError);
}
get statusOptions() {
return this.getStatusOptions();
}
}
3. 使用 RxJs 本身的操作符完成网络数据的缓存和更新
第一步:在模板中绑定触发更新的按钮
<div class="page-title" #triggerRefresh>
{{ "IoT Device" | safeTranslate : lang }}
</div>
第二步:给此按钮绑定触发更新的逻辑
fromEvent(this.triggerRefresh.nativeElement, 'click').pipe(
takeUntil(this.isComponentDestroyed),
tap(() => void this.adHocMoniListService.refreshData()),
catchError(() => of({}))
).subscribe();
注意这个过程是在 ngAfterViewInit 中完成的。
第三步:构建获取数据的方法
private formSubject = new BehaviorSubject(undefined);
formAction$ = this.formSubject.asObservable();
...
async fetchTableData(formData: FormData | undefined | any) {
this.showListSpin = false;
this.formSubject.next(formData);
}
第四步:将数据流和表单筛选的事件流结合起来
combineLatest([
this.adHocMoniListService.data,
this.formAction$,
]
).pipe(
map(([newData, formData]) => {
console.log('newData: ', newData);
console.log('formData: ', formData);
const rawData = [...newData, ...newData, ...newData];
let filteredData = rawData;
if (formData)
filteredData = rawData.filter(item => {
const matchesDescription = !formData?.desp || item.description.includes(formData.desp);
const matchesStatus = !formData?.type || item.status === formData.type;
const matchesType = !formData?.num || item.num === formData.num;
const matchesId = !formData?.id || item.id === formData.id;
console.log(matchesDescription, matchesStatus, matchesType, matchesId);
return matchesId && matchesType && matchesDescription && matchesStatus;
});
return filteredData;
})
).subscribe(
(filteredData) => {
this.dataSource.data = filteredData;
this.forceStyleChanging(0);
this.showListSpin = false;
}
)
第五步:在服务中实现接口数据的缓存以及重新获取数据的更新方法
import { Injectable } from '@angular/core';
import { PeriodicElement } from '../iotDeviceList/ad-hoc-moni-list.component';
import { BehaviorSubject, Observable, Subject, from, of } from 'rxjs';
import { FetchClient } from '@c8y/client';
import { catchError, mapTo, shareReplay, switchMap, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AdHocMoniListService {
private trigger = new BehaviorSubject(0);
error: any;
triggerAction$ = this.trigger.asObservable();
constructor(
private fetchClient: FetchClient,
) { }
private newData = [{
'id': 'EQPID#20240101',
'eui': '24E124723D4951091',
'description': 'AA',
'num': 'AABBCCDD',
'location': 'XXX',
'status': 'Running',
},
{
'id': 'EQPID#20240102',
'eui': '24E124723D4123456',
'description': 'BB',
'num': 'BBCCDDEE',
'location': 'XXX',
'status': 'Running',
},
{
'id': 'EQPID#20240103',
'eui': '24E122222D4950000',
'description': 'CC',
'num': 'CCDDEEFF',
'location': 'XXX',
'status': 'Running',
},
{
'id': 'EQPID#20240104',
'eui': '24E888888D3000000',
'description': 'DD',
'num': 'AABBCCDD',
'location': 'XXX',
'status': 'Offline',
},
];
get data(): Observable<PeriodicElement[]> {
return this.triggerAction$.pipe(
switchMap(() => from(this.fetchClient.fetch('service/dtm-ms/health')).pipe(
shareReplay(1),
mapTo(
this.newData
),
catchError(err => {
if (err) this.error = err;
return of([]);
})
))
)
}
refreshData(): void {
this.trigger.next(0);
}
}
4. 在合适的场景下使用 concatMap mergeMap switchMap
前端代码
from([1, 2, 3, 4]).pipe(
mergeMap(path => this.http.get(`/api/test/${path}`)),
toArray(),
).subscribe(data => {
console.log('mergeMap: ', data);
})
from([1, 2, 3, 4]).pipe(
concatMap(path => this.http.get(`/api/test/${path}`)),
toArray(),
).subscribe(data => {
console.log('concatMap: ', data);
})
后端代码:
app.get('/api/test/:path', (req, res) => {
// 获取URL参数 'path'
const path = req.params.path;
// 构造响应对象
const response = {
data: path
};
// 返回JSON响应
setTimeout(() => {
res.json(response);
}, Math.random() * 1000 << 2 >> 2)
});
结果对比: