从Angular6开始入门RxJS6

简介

本文的读者受众

  • 正准备学习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 } 中..`);
});
image

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';

结束

image

翻译自

  • https://qiita.com/MasanobuAkiba/items/a5026bd37603cc29e9e7

你可能感兴趣的:(从Angular6开始入门RxJS6)