简介
本文的读者受众
- 正准备学习Angular的人
- 想要知道Rx和RxJS相关知识的人
这篇文章是什么?
Angular使用RxJS标准库来有效地实现异步处理。
为了使用好 RxJS,需要考虑到与传统编程的不同之处。
就我而言,在什么都不知道的状态下阅读官方文档,我也不明白它的优势或具体用法。
不能理解的最大因素是我并没有形成RX的概念印象。
如果我从一开始就拥有这个概念印象,我认为我的学习会更顺利...... orz
所以在这篇文章中
- 我想现在开始使用Angular 6,但我还需要学习一个名为RxJS的库...
- 我想学习Rx,但我不知道从哪里开始
- 我想知道Rx是什么
将以这些方向,对RxJS使用的优点和他的概念、经常使用的方法进行解释。
RxJS
反应式扩展
Reactive Extensions(Rx)是,一个使用可观察数据序列和类LINQ样式的请求运算符,来创建(实现)异步及基于事件程序的技术库。
数据序列,拥有各种形式的存在。例如来自文件和Web服务的数据流,对Web服务的请求,系统通知以及用户操作的事件。
https://msdn.microsoft.com/en-us/library/hh242985(v=vs.103).aspx
- 在简单阅读完上面链接内容后,继续以下内容
在Angular中使用RxJS的优点
Rx的世界中,处理的值并非固定,而是可以不断变化的数据流。
您可以在流中放置任何内容,例如用户操作的事件值活API响应结果等异步值,或数字和字符串等同步值。
任何值都可以在数据流中流入,Rx提供的通用格式进行数据的加工和时机的处理。
无论是事件还是API响应,您都可以使用相同的格式并通过便捷的代码段编写外观漂亮的代码。
这是使用Rx的最大好处。
因为JavaScript缺少用于操作数组和对象的标准API,所以我认为有很多机会使用名为lodash
的库。
我认为Rx是Promise版的 lodash
。
我称之为“时序处理”的是具体的以下实现。
- 控制高速连续的事件在每50ms发生
- 控制经常出现浏览器滚动控件
- 最后一次检测到事件后100毫秒
- 频繁出现的表单相关处理
- 事件在一定时间内发生过多次
- 控制双击等
在Angular ( SPA )中,时序相关的处理是很频繁的,如果每次都使用标准的 setTimeout
等方法实现的话,会有很多性能的浪费,而且代码也会变得极其难以阅读、理解。
通过使用RxJS,您可以将所有数据的合并、过滤、映射与时间轴的处理,轻松地放在一起实现。
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();
}
桃子罐头
RxJS的概念总结
我认为可以在上一章的故事中大致理解Rx的概念,但我将再次回到原文。
在官方指南中,Rx库由以下公式表示:
Rx = Observables + LINQ (Operators) + Schedulers
正如我前面提到的,Observables是河流,事件,异步处理等的可观察对象,也是流的起点。
此外,Operators 可以被视为决定如何处理流中流动值的设备。
一旦订阅后,您可以使用反应式编程执行一系列流处理。
Subject
Subject类经常以各种方式使用,例如在Rx的逻辑中通知或临时存储值。
Subject结合上面的例子来说,类似于大坝。
大坝连接到河流,可以观察从大坝流出的价值,另外,也可以从外部设定值。
他就像流版的变量一样。
Subject有很多种类型,所以不能一概而论,但我认为它的概念印象是下面的图像。
重复出现的 Observable 和 Operators
如果上面说的已经全部理解的话,之后再有什么样的川 ( Observable ) 、什么样的装置( Operators ) 、剩下的工作仅仅是记住它们罢了。
这一次,我试图整理一个简单的使用场景,专注于我经常使用的东西。
此列表优先考虑具体的用途和印象,因为如果每个文本中都包含详细说明,则文字数量将是巨大的。
有关详细用法,请参阅官方文档。
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
// ...
count: {{ count$ | async }}
concat
- 保存流的顺序并结合
使用场景:在应用中保存缓存内容和API响应的合成,并立即显示缓存 → 切换到准确的数据
this.article$ = concat(this.store.select(getSelectArticle), this.articleDb.findByKey(articleKey));
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
// ...
count: {{ count$ | async }}
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);
关于RxJS6的导入
Observable、Subject 以及 Subscription
import { Observable, concat, Subject, Subscription } from 'rxjs';
Operators
import { map, tap } from 'rxjs/operators';
结束
翻译自
- https://qiita.com/MasanobuAkiba/items/a5026bd37603cc29e9e7