Angular 入门之 RxJS (基础篇)

130 阅读6分钟

正确的理解编程的本质才能在早下班的同时卷死别人 - 麦克阿瑟

麦克阿瑟: 不,我没说过!

什么是RxJS

废话少说,先看定义:

RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators inspired by Array methods (mapfilterreduceevery, etc) to allow handling asynchronous events as collections.

直译过来就是:

RxJS 是一个使用可观察序列编写异步和基于事件的程序的库。它提供了一种核心类型,即 Observable、卫星类型(观察者、调度程序、主题)和受数组方法(map、filter、reduce、every 等)启发的运算符,以允许将异步事件作为集合处理。

说人话就是一种新的异步编程的方式,也就是流式方式。

结构

RxJS 具备一个相对固定的代码结构。

  Source.pipe().subscribe();
  • Source 部分

Source也就是数据源,一个完整的流至少要有一个数据源。 RxJS中可以创建数据源的方式我个人归类为三种。

  1. 实例化
import { Observable } from 'rxjs';

const source = new Observable((observer) => {
  // some logic...
  observer.next('result');
});

source.subscribe(console.log); // result

// Or

const hello = Observable.create(function(observer) {
    observer.next('Hello');
    observer.next('World');
    observer.complete();
});

这种写法非常类似于我们实例化一个Promise的写法。

new Promise((resolve, reject) => {
  // some logic 
  resolve('result');

  if(something wrong) {
    reject()
  }
})

自己构造一个流在有些场景下是一个合理的解决方案,但是大部分场景下其实我们不需要自己构造。

  1. 操作符

RxJS提供了一些操作符可以直接创建一个源流。

e.g.

Tips: 下面所有的output 都是订阅之后获取到的结果,并不是定义之后就能获取的结果!!!

  • of是最简单的构造操作符,接收一个值,然后在流中直接输出该值
of('something'); //output: something

  • from类似于of它也可以接收值,但是只处理数据,即使传入字符串也会按照数组一个个输出。同时它还可以接收Promise和Map,也就是可以实现将Promise转换成Observerable.
from([1,2,3,4]); //output: 1,2,3,4
from(new Promise(resolve => resolve('Hello World!'))); //output: 'Hello World'
from(new Map()); //output: [key, value] in Map
  • fromEvent顾名思义它用来处理指定的target上的事件响应。这里的事件也可以是我们自定义的事件。
fromEvent(document, 'click');
fromEvent(window, 'resize');
fromEvent(window, 'online');
fromEvent(target, 'customEvent');
  • interval类似于setInterval构造一个不断产生数据的源, 传入的参数是间隔。 下面的例子就是每秒流出一个值,从0开始自增。
interval(1000); //output: 0,1,2,3,4,5....

操作符构建源流都是比较类似的结构,他们都是开始的操作符并且接收一些数据结构。

  1. Subject

Subjet类似于操作符,但是又有一些自己的特性。所以我们单独拿出来讲。

上面构造出来的源流,一但构造结束之后就不能再进行修改了。所以当我们有一些逻辑会影响源的时候,我们需要设置源是一种可以修改的流。Subject 就是这样的一个流操作符。

  • Subject 创造一个源,用户可以自己主动往源里面增加值,这样已经订阅的地方就能收到这个值。如果在值被push进Subject之前没有订阅就不能获取到该值。
const sub = new Subject();

sub.next(1); //output: 1
sub.next(2); //output: 2
sub.next(3); //output: 3
  • BehaviorSubject 相对于前面的 Subject, BehaviorSubject 无论你什么时候订阅都会返回最近的一次结果。需要注意的一点就是BehaviorSubject需要一个默认值。
const subject = new BehaviorSubject(123);
// subject.subscribe();// output: 123, 456, 789
subject.next(456);
subject.next(789);
// subject.subscribe();// output: 789
  • ReplaySubject相对于前面的BehaviorSubjectReplaySubject可以支持重放最近的指定的个数个值。例如,我需要重放最近的两个值,三个值。
const sub = new ReplaySubject(3);
sub.next(1);
sub.next(2);
sub.subscribe(console.log); // OUTPUT => 1,2
sub.next(3); // OUTPUT => 3
sub.next(4); // OUTPUT => 4
sub.subscribe(console.log); // OUTPUT => 2,3,4 (log of last 3 values from new subscriber)
sub.next(5); // OUTPUT => 5,5 (log from both subscribers)

操作符

  • tap一个不会影响流数据的只读操作符。
   interval(1000).pipe(
       tap((value: number) => {
           // do something you need
           // 不会影响数据流
       })
   ).subscribe();

  • map 按照值类型进行直接处理,不会进行拆包
   interval(1000).pipe(
       map((value: number) => {
           return value * 10;
       })
   ).subscribe(); // output: 0, 10, 20, 30 ...

image.png

  • mapTo 将流内的值映射到另一个值
   interval(1000).pipe(
       mapTo('遥遥领先')
   ).subscribe(); // output: 遥遥领先, 遥遥领先, 遥遥领先, 遥遥领先 ...

  • switchMap 从一个流切换到另一个流,并结束前一个流。需要注意的是switchMap的返回值一定要是一个Observable
fromEvent(document, 'click')
    .pipe(
    // restart counter on every click
    switchMap(() => interval(1000))
    )
    .subscribe(console.log);

image.png

  • mergeMap/flatMap 合并一个新的流到主流中
const click$ = fromEvent(document, 'click');

click$.pipe(
  mergeMap(() => ajax.getJSON(API_URL))
)
.subscribe(console.log);

image.png

  • take/takeUntil/takeWhile 达到结束条件之后结束当前流。条件可以是次数或者条件
   interval(1000).pipe(
       mapTo('遥遥领先'),
       take(5) // 只执行5次,然后结束
   ).subscribe(); // output: 遥遥领先, 遥遥领先, 遥遥领先, 遥遥领先 遥遥领先

   const stop$ = new Subject();

   interval(1000).pipe(
       mapTo('遥遥领先'),
       takeUntil(stop$)
   ).subscribe(); // output: 遥遥领先, 遥遥领先, 遥遥领先, 遥遥领先 遥遥领先
   
   stop$.next(); // 会触发上述流的结束
  • withLatestFrom源数据触发的时候获取另一个流内的最新值。
const source = interval(5000);

const secondSource = interval(1000);

const example = source.pipe(

withLatestFrom(secondSource),

map(([first, second]) => {
    return `First Source (5s): ${first} Second Source (1s): ${second}`;
})
);

/*

"First Source (5s): 0 Second Source (1s): 4"

"First Source (5s): 1 Second Source (1s): 9"

"First Source (5s): 2 Second Source (1s): 14"

...

*/

const subscribe = example.subscribe(val => console.log(val));

代码风格(个人推荐)

RxJS的核心想思想就流式处理,所以风格的核心就是干净的流处理逻辑。

怎么理解干净的流处理逻辑呢?比如你的处理逻辑里面乱七八糟的管道,嵌套会让流的顺序变得很乱。

我个人推崇能合并到主流里面去顺序操作的一定要合并到主流来操作。

举个例子:

// Bad Case
of('start').pipe(
    switchMap(() => 
        http.get('some/data').pipe(map((data) => convertData(data)))
    )
).subscribe();

// Prefer
of('start').pipe(
    switchMap(() => http.get('some/data')),
    map((data) => convertData(data))
).subscribe();

就像上面的convertData方法,应该写在主流里面,在获取到数据之后,而不应该写在子流的处理逻辑里面。当然,上面这个例子并不绝对。在多子流同时操作的时候,每个子流可能有不同的处理逻辑,是需要写在子流里面的。

所以一个理想的流处理结构应该是下面这种结构:


    source.pipe(
        opeartor(),
        opeartor(),
        opeartor(),
        opeartor(),
        opeartor(),
    ).subscribe()

这种结构下,这段代码的整体逻辑会变得很清楚,可读性很高。

对于subscribe里面的函数,我个人不推荐在里面写业务逻辑。如果只是非常简单的值操作,可以放这里,但是其他的还是尽量避免。

想要写出维护性高的代码,就要对代码的风格和形式有一定的追求。

Mien, 6年前端开发经验,目前就职于SLB。 涉猎范围: Angular, Vue, React, 小程序, Three.js, Cesium.js, Babylon.js, electron, Tauri, nodeJS, Rust以及一些乱七八糟的技术 欢迎留言提问和指正。

我是Mien,我们下期再见。