https://www.jianshu.com/p/bcff2bd2b0d6
1 简介
1.1 本文的读者受众
- 正准备学习
Angular
的人 - 想要知道
Rx
和RxJS
相关知识的人
1.2 这篇文章是什么?
Angular
使用RxJS
标准库来有效地实现异步处理。
为了使用好 RxJS
,需要考虑到与传统编程的不同之处。
就我而言,在什么都不知道的状态下阅读官方文档,我也不明白它的优势或具体用法。
不能理解的最大因素是我并没有形成RX
的概念印象。
如果我从一开始就拥有这个概念印象,我认为我的学习会更顺利… orz
所以在这篇文章中
- 我想现在开始使用
Angular 6
,但我还需要学习一个名为RxJS
的库… - 我想学习
Rx
,但我不知道从哪里开始 - 我想知道
Rx
是什么
将以这些方向,对RxJS
使用的优点和他的概念、经常使用的方法进行解释。
1.3 RxJS
反应式扩展
Reactive
Extensions(Rx)
是,一个使用可观察数据序列和类LINQ样式的请求运算符,来创建(实现)异步及基于事件程序的技术库。
数据序列,拥有各种形式的存在。例如来自文件和Web服务的数据流,对Web服务的请求,系统通知以及用户操作的事件。
https://msdn.microsoft.com/en-us/library/hh242985(v=vs.103).aspx
在简单阅读完上面链接内容后,继续以下内容
1.4 在Angular中使用RxJS的优点
Rx
的世界中,处理的值并非固定,而是可以不断变化的数据流。
您可以在流中放置任何内容,例如用户操作的事件值活API
响应结果等异步值,或数字和字符串等同步值。
任何值都可以在数据流中流入,Rx提供的通用格式进行数据的加工和时机的处理。
无论是事件还是API
响应,您都可以使用相同的格式并通过便捷的代码段编写外观漂亮的代码。
这是使用Rx的最大好处。
因为JavaScript
缺少用于操作数组和对象的标准API
,所以我认为有很多机会使用名为lodash
的库。
我认为Rx
是Promise版的 lodash
。
我称之为“时序处理”的是具体的以下实现。
- 控制高速连续的事件在每
50ms
发生 - 控制经常出现浏览器滚动控件
- 最后一次检测到事件后
100
毫秒 - 频繁出现的表单相关处理
- 事件在一定时间内发生过多次
- 控制双击等
在Angular ( SPA )
中,时序相关的处理是很频繁的,如果每次都使用标准的 setTimeout
等方法实现的话,会有很多性能的浪费,而且代码也会变得极其难以阅读、理解。
通过使用RxJS
,您可以将所有数据的合并、过滤、映射与时间轴的处理,轻松地放在一起实现。
1.5 Rx的印象
理解事物最重要的是印象的转换。
在本章中,我们将之前所罗列的Rx概念升华为印象。
这次我为那些根本不了解Rx
的人做了一个简单的故事。
通过跟踪这个故事,我认为您可以学习到Rx的粗略概念。
- 在夏季时,有一条流淌桃子的河流(
stream
) - 当然,除了桃子 (
value
) 以外也有魚 (value
)在流动。 - 在秋、冬、春时不会流淌桃子
- 您想利用这条神秘的河流,制造出材料成本为
0
的桃子罐头,然后贩卖赚大钱。 - 因此,我们需要建立一个系统,可以自动从河流中收集 (
filter
)桃子、将其转换成桃子罐头 (map
) - 系统的运行需要消耗电力
- 夏天的时候运行系统 (
subscribe
) - 夏天以外的时候不会有桃子,所以要关闭系统(
unsubscribe
),以便他不消耗不必要的电力
Rx
最重要的概念为“流”,所以经常用河流做比较。
现实中的河流,在没有我们做任何事情的情况下也会继续自由地流动。
像上面的故事中的一样,管理者打开装载的开关,监视 (subscribe
) 河流、在罐头制造装置 (operators
) 接收桃子(value
) 时,执行期望的处理。
Rx
初学者刚开始经常遇到的问题是,因为忘记使用subscribe
,而一直在查找值不流出的原因。
但是,如果您从一开始就能想到故事,就不会把时间浪费在这种类似的错误上。
此外,故事中出现的电力,就现实而言也就是客户端的CPU
资源。与每个月发生的电费不同,内存泄漏经常出现在债务堆积的情况下。
因此,当您销毁组件时,请务必记住取消订阅内部订阅的流。
最后,将此故事转换为实际代码如下所示:
private subscription: Subscription;
ngOnInit() {
this.subscription = of('桃子', '鲤鱼').pipe(
filter(v => v === '桃子'),
map(v => v + '罐头')
).subscribe(console.log);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
桃子罐头
2 RxJS的概念总结
我认为可以在上一章的故事中大致理解Rx
的概念,但我将再次回到原文。
在官方指南中,Rx
库由以下公式表示:
Rx = Observables + LINQ (Operators) + Schedulers
正如我前面提到的,Observables
是河流,事件,异步处理等的可观察对象,也是流的起点。
此外,Operators
可以被视为决定如何处理流中流动值的设备。
一旦订阅后,您可以使用反应式编程执行一系列流处理。
2.1 Subject
Subject
类经常以各种方式使用,例如在Rx的逻辑中通知或临时存储值。
Subject
结合上面的例子来说,类似于大坝。
大坝连接到河流,可以观察从大坝流出的价值,另外,也可以从外部设定值。
他就像流版的变量一样。
Subject
有很多种类型,所以不能一概而论,但我认为它的概念印象是下面的图像。
2.2 重复出现的 Observable 和 Operators
如果上面说的已经全部理解的话,之后再有什么样的川 ( Observable
) 、什么样的装置( Operators
) 、剩下的工作仅仅是记住它们罢了。
这一次,我试图整理一个简单的使用场景,专注于我经常使用的东西。
此列表优先考虑具体的用途和印象,因为如果每个文本中都包含详细说明,则文字数量将是巨大的。
有关详细用法,请参阅官方文档。
2.2.1 Observable
from
功能:将Promise
或者 iterator
的值 ( string
、array
等 ) 转换为 Observable
.
使用例:处理API
响应结果并检索所需的值
from(
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(r => r.json())
).map(v => v. userId).subscribe(console.log);
// 1
fromEvent
功能:将event
转换为 Observable
使用例:双击的捕捉
const click$ = fromEvent(document, 'click');
click$.subscribe(console.log);
merge
功能:合并流动的值
使用场景:将各种事件合成为一个触发器
const click$ = fromEvent(targetElement, 'click');
const mouseover$ = fromEvent(targetElement, 'mouseover');
merge(click$, mouseover$).subscribe(() => {
// 期望的处理
});
of
功能:将值转换为 Observable
使用场景:测试、确认用、流分裂或结合时的搭配
const hoge$ = of(1, 2, 3);
const huga$ = fromEvent(document, 'click');
merge(hoge$, huga$).subscribe(console.log);
interval
功能:定时流动的值
使用场景:显示已用时间
this.count$ = interval(1000);
// wait 1sec
// 0
// wait 1sec
// 1
// wait 1sec
// 2
// ...
<div>count: {{ count$ | async }}</div>
concat
功能:保存流的顺序并结合
使用场景:在应用中保存缓存内容和API响应的合成,并立即显示缓存 → 切换到准确的数据
this.article$ = concat(this.store.select(getSelectArticle), this.articleDb.findByKey(articleKey));
2.2.2 Operators
tap(旧版do)
功能:在不影响流的情况下进行任何处理
使用场景:日志显示
stream$
.pipe(
tap(console.log),
tap(console.warn),
tap(console.error),
)
.subscribe();
map / pluck
功能:流的値的加工・转换・抽出
使用场景:处理API
响应结果(抽出需要的值)
const apiResponse$ = of({ userId: 1, body: 'hoge huga piyo' });
const userId$ = apiResponse$.pipe(map(v => v.userId));
// ---------------------------------------------------
// 如果只想要取得值,可以使用pluck,让代码更简洁
const userId$ = apiResponse$.pipe(pluck('userId'));
filter
功能:过滤值
使用场景:重定向事件时显示加载进度条
const routerEvent$ = this.router.events;
routerEvent$
.pipe(filter(e => e instanceof NavigationStart))
.subscribe(() => this.store.dispatch(new ShowLoadingSpinnerAction()));
skip
功能:跳过值
使用场景:跳过组件生成后联动处理
// 跳过第一次流动的值,因为它不是用户操作更改的值
this.route.params.pipe(pluck('categoryId'), skip(1)).subscribe(categoryId => {
console.log(`changed categoryId: ${ categoryId }`);
});
scan
功能:使用以前的值
使用场景:无限滚动条的项目列表管理
this.items$ = nextItemSubject$.scan((acc, curr) => {
return acc.concat(curr);
}, []);
take
功能:确定值流动的次数
使用场景:只使用变动值的最初 x 回
// 如果不使用take(1) 的话,每回store值更新的时候,都会调用API
this.store.select(getUserId).pipe(
take(1),
concatMap(userId => this.apiService.get(userId))
).subscribe();
startWith
功能:指定最初流动的值
使用场景:显示经过的时间(改良版)
※ 如果仅使用 interval
,则第一秒将不会显示任何内容。
this.count$ = interval(1000).pipe(map(v => v + 1), startWith(0));
// 0
// wait 1sec
// 1
// wait 1sec
// 2
// ...
<div>count: {{ count$ | async }}</div>
takeUntil
功能:值流动时的暂停处理
使用场景:在销毁组件时通过Subject
发送结束流的通知
※ 但请注意、这篇文章介绍的内存泄露
private onDestroy$ = new Subject();
ngOnInit() {
interval(1000).pipe(takeUntil(this.onDestroy$)).subscribe(console.log);
}
ngOnDestroy() {
this.onDestroy$.next();
}
concatMap
功能:将值转变成Observable
后合并(执行中的处理结束后转到下一个操作处理)
使用场景:使用API1响应结果调用API2
switchMap
功能:将值转变成Observable
后合并(下一个値过来时,中断正在执行的处理)
使用场景:实现 auto complete 功能
debounceTime
功能:弃掉在两次输出之间小于指定时间的发出值
使用场景:实现 auto complete 功能
this.autoCompleteList$ = this.form.valueChanges.pipe(
debounceTime(100),
switchMap(input => this.apiService.get(input)),
);
throttleTime
功能:控制值流动的速度
使用场景:控制滚动条事件
fromEvent(window, 'scroll').pipe(throttleTime(50)).subscribe(console.log);
withLatestFrom
功能:与合并后流最新的值进行合并
使用场景:将点击的用户ID作为GA事件发送
fromEvent(targetElement, 'click').pipe(
withLatestFrom(this.store.select(getUserId))
).subscribe(([_, userId]) => {
this.analyticsService.sendEvent({ category: 'test', action: 'click', userId });
})
combineLatest
功能:如果对主流和合成流都进行了更改,则会发送每个流的最新值
使用场景:表单输入值和store信息的合并
this.form.valueChanges.pipe(
combineLatest(this.store.select(getUserId))
).subscribe(([input, userId]) => {
console.log(`用户userId( ${ userId } ) 输入${ input } 中..`);
});
publish, share, refCount…etc
cold
流转化为hot
流
cold / hot
的概念在这篇文章 中介绍。
使用场景:使用API1
响应结果并行执行API2
和API3
※ 将 API1
流 hot
化后,可防止多次调用API1
const userId$ = from(fetch('https://jsonplaceholder.typicode.com/posts/1').then(r => r.json())).pipe(
pluck('userId'),
publishReplay(1),
refCount()
);
userId$.concatMap(userId => this.api2Service.get(userId)).subscribe(console.log);
userId$.concatMap(userId => this.api3Service.get(userId)).subscribe(console.log);
3 关于RxJS6的导入
Observable
、Subject
以及 Subscription
import { Observable, concat, Subject, Subscription } from 'rxjs';
Operators
import { map, tap } from 'rxjs/operators';