我的 rxjs 心路历程

442 阅读12分钟

前言

这段时间在项目中接触到了 rxjs, 一开始接触 rxjs 时,是非常抗拒的,心想这是啥玩意,咋这么难读懂。渐渐了解 rxjs,发现这玩意是真的好用呀,因此通过写文章的形式巩固自己对 rxjs 的理解。

是什么?

RxJS 是一个基于可观测数据流 Stream 结合观察者模式和迭代器模式的一种异步编程的应用库。RxJSReactive ExtensionsJavaScript 上的实现。

基本概念:

  • Observable(可观察对象): 表示一个可调用的未来值或事件的集合。
  • Observer(观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。
  • Subscription(订阅): 表示Observable的执行,它主要用于取消 Observable 的执行。
  • Operator(操作符): 采用函数式编程风格的纯函数,使用像 map、filter、concat 等这样的操作符来处理集合。
  • Subject(主体): 相当于EventEmitter,并且是将值或事件多路推送给多个Observer。
  • Schdulers(调度器): :用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeout 或 requestAnimationFrame。

怎么用?

Observable

Observable 是由多个值组成的延迟推送集合。

创建 Observable

可以通过构造函数创建 Observable, 该构造函数接受一个参数:subscriber

import { Observable } from "rxjs";

const observable = new Observable((subscriber) => {
  setInterval(() => {
    subscriber.next(1);
  }, 1000);
});

observable.subscribe((val) => {
  console.log(`observable, `, val);
});

image.png

从上面例子我们看出,可以通过构造函数创建 Observable,每秒会发出值 1。

import { Observable } from "rxjs";

const observable = new Observable((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.next(4);
  setTimeout(() => {
    subscriber.next(5);
  });
});

console.log("start");
observable.subscribe((val) => {
  console.log(`observable, `, val);
});
console.log("end");

image.png

从输出结果来看, Observable 能同步或异步地传递值

Observer

Observer 是 Observable 传递值的消费者。观察者只是一组回调,对应于 Observable 传递的每种类型的通知:nexterrorcomplete

// 形式如下
const observer = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
  complete: () => console.log('Observer got a complete notification'),
};

// 使用
observable.subscribe(observer);

从👆上面例子来看, 观察者是具有三个回调的对象,用于 Observable 可能传递的每种类型的通知。

我们在调用subscribe的时候可以使用这两种方式,以一个对象形式,该对象具备nexterrorcomplete三个方法(都是可选的),或者直接传入函数的方式,参数前后分别为nexterrorcomplete

如果发送了错误 error 或完成通知 complete,则之后无法发送任何其他通知。

Subject

Subject 和 Observable 类似,你可以订阅 Subject,它会像订阅 Observable 一样。它也有类似 next()、error() 以及 complete() 的方法,就像你平时传给 Observable 构造函数的 Observer 一样。

但 Subject 主要是为了多播,而 Observable 默认是单播的,即对于每个订阅者,都只有一个独立的 Observable 与之对应。

import { Observer, Observable, Subject } from "rxjs";

const observable = Observable.create(
  (observer: { next: (arg0: number) => void }) => {
    observer.next(Math.random());
  }
);

// 订阅者1
observable.subscribe((data: any) => {
  // 每当调用一次 observable.subscribe 就会有一个新的 Observable 产生, 即俩次的data是不一样的
  // 因为 Observable 在设计上就是单播
  console.log(`data1`, data);
});

// 订阅者2
observable.subscribe((data: any) => {
  console.log(`data2`, data);
});

执行结果:

image.png

通过结果我们可以知道,每当我们调用一次 Observable.subscribe() 时,一个新的 Observable execution 就会被启动。 因此,如果你希望使多个订阅者收到相同的数据,那么使用 Subject 是一个不错的选择。

当我们订阅 Subject 的时候,并不会像 Observable 一样,创建多个发布者。

import { Observer, Observable, Subject } from "rxjs";

// Subject 的设计上是多播,当我们订阅Subject时,他只会在现有的观察者列表中多注册一个新的观察者
const subject = new Subject();

// subject.next(400); // 写在这儿不会触发

subject.subscribe((data) => {
  // data3/data4的值是一样的
  console.log(`data3`, data);
});

subject.subscribe((data) => {
  console.log(`data4`, data);
});

subject.next(Math.random());

执行结果: image.png

通过结果我们可以发现,当我们订阅 Subject 时,并不会像 Observable 那样产生多个发布者。

当然,这并不是 Subject 的唯一用途,例如它还可以将一个单播 Observable 转化为多播。

import { Observer, Observable, Subject } from "rxjs";

const observable = Observable.create(
  (observer: { next: (arg0: number) => void }) => {
    observer.next(Math.random());
  }
);

// Subject 的设计上是多播,当我们订阅Subject时,他只会在现有的观察者列表中多注册一个新的观察者
const subject = new Subject();

// observable.subscribe(subject); 👉 放在这儿不会触发 subscribe

subject.subscribe((data) => {
  // data3/data4的值是一样的
  console.log(`data3`, data);
});

subject.subscribe((data) => {
  console.log(`data4`, data);
});

// 借助 Subject 将 Observable 转化为多播
observable.subscribe(subject);

我们将 Observable 的订阅者改成 Subject, 由 Subject 将事件传递给订阅者,从而将 Observable 转化为单播。

BehaviorSubject

BehaviorSubject 是 Subject 的一种形式,其特点是会存储当前的值,每当有订阅事件时,那么 BehaviorSubject 将给订阅者发送当前存储的值。

import { BehaviorSubject } from "rxjs";

const subject = new BehaviorSubject(50); 

subject.subscribe((data) => {
  console.log("Subject A", data);
});

subject.next(100); // BehaviorSubject

subject.subscribe((data) => {
  // 只要订阅了BehaviorSubject, BehaviorSubject会直接返回给订阅者当前存储的值
  console.log("Subject B", data);
});

subject.next(200);

console.log(subject.value); // 获取上一次的广播的值

输出:

image.png

订阅了BehaviorSubject, BehaviorSubject会直接返回给订阅者当前存储的值

ReplaySubject

ReplaySubject 相对于 BehaviorSubject 而言,ReplaySubject 可以发送旧数据,并可以指定存储的数量过期时间

import { ReplaySubject } from "rxjs";
const replaySubject = new ReplaySubject(2); // 指定只存储2个旧的值

// 订阅者A
replaySubject.subscribe((data) => {
  console.log(`Subscriber A:`, data);
});

replaySubject.next(1);
replaySubject.next(2);
replaySubject.next(3);

// 订阅者B
replaySubject.subscribe((data) => {
  // 如果之前有存储的值,会触发订阅
  console.log(`Subscriber B:`, data);
});

replaySubject.next(4);

输出:

image.png

订阅了ReplaySubject,ReplaySubject会指直接将存储的值返回给订阅者

AsyncSubject

AsyncSubject 只会在 Observable 完成后,将其最终值发给订阅者

import { AsyncSubject } from "rxjs";
const asyncSubject = new AsyncSubject();
// 订阅者A
asyncSubject.subscribe((data) => {
  console.log(`Subscriber A:`, data);
});

asyncSubject.next(1);
asyncSubject.next(2);
asyncSubject.next(3);

// 订阅者B
asyncSubject.subscribe((data) => {
  console.log(`Subscriber B:`, data);
});

asyncSubject.next(4);
asyncSubject.complete();

image.png

常见的操作符

操作符本质上是一个纯函数,它将一个 Observable 作为输入并生成另一个 Observable 作为输出。这是一个纯粹的操作:之前的 Observable 保持不变。

Piping(管道)

管道运算符是一个函数,它类似于 Gulp 中的 pipe,可以将功能进行流水线式的组合。例如:

observable.pipe(fn1, fn2, fn3, fn4);

创建运算符

创建运算符是可以用来创建 Observable 的函数。

interval

interval 创建的 Observable 可以在指定时间内发出连续的数字,其实就跟我们使用setInterval这种模式差不多。在我们需要获取一段连续的数字时,或者需要定时做一些操作时都可以使用该操作符实现我们的需求

import { interval } from "rxjs";

const observable = interval(1000 /* number of milliseconds */);

observable.subscribe((data) => {
  console.log(`interval: `, data); // 从 0 开始,每一秒会输出一个数字
});

of

of 会创建一个 Observable, 输出的值取决于传入的参数。类似于 interval,它和 pipe 很相似,有流水线的味道。

const observableOf = of(1, 2, 3);
observableOf.subscribe((data) => {
  console.log(`observableOf`, data); // 依次输出 1、2、3
});
from

from 可以将数组、类数组、Promise、迭代器对象或者类 Observable 对象转化并产生新的 Observable。

const observableFrom = from([1, 2, 3]);
observableFrom.subscribe((data) => {
  console.log(`observableFrom: `, data); // 依次输出1、2、3
});

const observableFromPromise = from(
  new Promise((resolve) => setTimeout(() => resolve(1), 3000))
);

observableFromPromise.subscribe((data) => {
  console.log(`observableFromPromise`, data); // 3s 后输出1
});

fromEvent

创建一个 Observable,该 Observable 发出来自给定事件对象的指定类型事件。可用于浏览器环境中的Dom事件或Node环境中的EventEmitter事件等。

const observableFromEvent = fromEvent(document, "click");
observableFromEvent.subscribe((data) => {
  console.log(`observableFromEvent: `, data);
});
rang

创建一个新的 Observable 并指定范围内的数字序列。

const observableRange = range(1, 4);
observableRange.subscribe((data) => {
  console.log(`observableRange: `, data);
});

image.png

empty

该操作符创建一个什么数据都不发出,直接发出完成通知的操作符。

repeat

创建一个新的 Observable 重复源上发出的所有值。就像 retry,但是用于非错误情况

import { of } from "rxjs";
import { repeat } from "rxjs/operators";
const observableRepeat = of(1, 2, 3).pipe(repeat(3));

observableRepeat.subscribe((data) => {
  console.log(`observableRepeat`, data); // 依次输出 1、2、3
});

image.png

转化运算符

buffer

收集过往的数据存放到数组中,仅当 buffer 传入的 Observable 发出通知时,才会发出此数组。这相当于一个缓冲区,将数据收集起来后,等到一个信号来临在释放出去

import { interval, fromEvent } from "rxjs";
import { buffer } from "rxjs/operators";

const myInterval = interval(1000);
const bufferBy = fromEvent(document, "click");

const myBufferedInterval = myInterval.pipe(buffer(bufferBy));

const subject = myBufferedInterval.subscribe((data: any) => {
  console.log(`Buffered values: `, data);
});

结果:

image.png

concatMap

concatMap 接收一个函数,该函数将值映射成内部 Observable,并按顺序依次订阅和发出。

// 发出延迟值
const sourceConcatMap = of(2000, 1000);
// 将内部 observable 映射成 source,当前一个完成时发出结果后订阅下一个
const resultConcatMap = sourceConcatMap.pipe(
  concatMap((val) => of(`Delayed by: ${val}ms`).pipe(delay(val)))
);
// 输出: With concatMap: Delayed by: 2000ms, With concatMap: Delayed by: 1000ms
resultConcatMap.subscribe((val) => console.log(`With concatMap: ${val}`));

结果:

image.png

mergeMap

mergeMap 和 concatMap 类似,接受一个函数,该函数将值映射成内部 Observable。但和 concatMap 有区别,mergeMap 并不会等上一次的订阅完成后,在进行下一次的订阅。

const source = of(2000, 1000);
const exampleConcatMap1 = source.pipe(
  concatMap((val) => of(`Delayed by: ${val}ms`).pipe(delay(val)))
);
exampleConcatMap1.subscribe((val) => {
  console.log(`With concatMap: ${val}`);
});

const mergeMapExample = source.pipe(
  delay(5000), // 确保比 concatMap 晚执行
  mergeMap((val) => of(`Delayed by: ${val}ms`).pipe(delay(val)))
);
mergeMapExample.subscribe((val) => {
  console.log(`With mergeMap: ${val}`);
});

image.png

map

map 接受一个函数,该函数可以将值进行转化,返回值即可作为订阅者实际获取到的值。

const sourceMap = interval(1000).pipe(take(3));
const resultMap = sourceMap.pipe(map((x) => x * 2));
resultMap.subscribe((x) => {
  console.log(`resultMap: `, x); // 每隔 1 s 依次输出 0/2/4
});
mapTo

mapTo 忽略数据源发送的数据,将每个发出值映射成常量

const sourceMapTo = interval(1000).pipe(take(3));
const resultMapTo = sourceMapTo.pipe(mapTo("Hello world"));
resultMapTo.subscribe((data) => {
  console.log(`resultMapTo: `, data);
});
switchMap

switchMap 主要作用首先会对多个 Observable 进行合并,并且具备打断能力,也就是说合并的这个几个Observable,某个 Observable 最先开始发送数据,这个时候订阅者能正常的接收到它的数据,但是这个时候另一个 Observable 也开始发送数据了,那么第一个 Observable 发送数据就被打断了,只会发送后来者发送的数据。

const btn = document.createElement("button");
btn.innerText = "try try";
document.body.append(btn);
const sourceSwithMap = fromEvent(btn, "click");
const resultSwithMap = sourceSwithMap.pipe(
  switchMap(() => interval(1000).pipe(take(3)))
);
resultSwithMap.subscribe((data) => {
  console.log(`resultSwithMap `, data);
});

image.png

scan

scan 是累加器操作符,作用类似于 reduce 函数

const sourceScan = of(1, 2, 3);
const exampleScan = sourceScan.pipe(scan((acc, curr) => acc + curr, 0));
exampleScan.subscribe((val) => {
  console.log(`exampleScan: `, val); // 依次输出 1、3、6
});

过滤操作符

take

只发出源 Observable 最初发出的的 N 个值。

import { interval } from "rxjs";
import { take } from "rxjs/operators";

interval(1000)
  .pipe(take(3))
  .subscribe((data) => {
    console.log(`data`, data);
  });

image.png

skip

skip 返回一个 Observable, 会跳过前面的 N 个值。

from([1, 2, 3, 4, 5])
  .pipe(skip(2))
  .subscribe((data) => {
    console.log(`skip`, data);
  });

image.png

filter

对 Observable 发出的值进行过滤,类似于数组的 filter。

from([1, 2, 3, 4, 5])
  .pipe(filter((x) => x > 3))
  .subscribe((data) => {
    console.log(`filter`, data);
  });

结果:

image.png

distinct

类似于 Set 的作用,过滤重复的值

from([1, 2, 2, 3, 3, 4])
  .pipe(distinct())
  .subscribe((val) => {
    console.log("distinct", val);
  });

结果:

image.png

debounceTime

debounceTime 和防抖类似,在规定的时间内 Observable 持续发出,只会接受最后一个值

interval(1000)
  .pipe(take(3), debounceTime(2000))
  .subscribe((data) => {
    console.log(`debounceTime`, data); // 第 5 秒的时候发出值 2
  });
throttleTime

throttleTime 和节流类似,在规定的时间内只会发出一个值。

interval(1000)
  .pipe(take(3), throttleTime(2000))
  .subscribe((data) => {
    console.log(`throttleTime`, data); // 依次输出 0 和 2
  });

组合操作符

concatAll

concatAll 会顺序将依次合并合并的 Observable, 将高阶的 Observable 扁平化。

const source = interval(2000);
const example = source.pipe(
  map((val) => of(val + 10)),
  concatAll() // 合并内部的 observable 的值
);
example.subscribe((val) => {
  console.log(`val: `, val);
});

结果:

image.png

如何去除了 concatAll 将返回一个 Observable。

const source = interval(2000);
const example = source.pipe(
  map((val) => of(val + 10))
  // concatAll() // 合并内部的 observable 的值
);
example.subscribe((val) => {
  console.log(`val: `, val);
});

结果: image.png

zip

zip 会将多个 Observable 组合以创建一个 Observable,返回值类似于 Promise.all, 值是由所有输入 Observables 的值按顺序计算而来的。

const s1 = interval(1000).pipe(take(3));
const s2 = interval(1000).pipe(take(5));
const result = zip(s1, s2);
result.subscribe((data) => {
  console.log(`data: `, data);
});

只要有一个 Observable 结束, 则 zip 就结束。

image.png

startWidth

startWidth 会往 Observable 插入第一个值。

const sourceStartWith = interval(1000).pipe(take(3));
const resStartWith = sourceStartWith.pipe(startWith(666));
resStartWith.subscribe((data) => {
  console.log(`data => `, data);
});

image.png

多播操作符

multicast

使用 multicast 操作符返回一个 Subject,可以将一个单播的 Observable 转化为多播的形式。


const source = interval(500).pipe(take(5));
const multicasted = source.pipe(
  multicast(new Subject())
) as ConnectableObservable<number>;

const subscriptionA = multicasted.subscribe((v) => console.log("A: " + v));
const subscriptionB = multicasted.subscribe((v) => console.log("B: " + v));
const subscriptionConnect = multicasted.connect();

setTimeout(() => {
  subscriptionA.unsubscribe();
  subscriptionB.unsubscribe();
  // subscriptionB退订后,source已经没有订阅者了,要加上这句才是真正的退订
  subscriptionConnect.unsubscribe();
}, 3000);

image.png

refCount

如果觉的 multicast必须调用 connect方法才能推送值,还要 multicasted.unsubscribe()才能真正结束推送有些麻烦,就可以用 refCount。

refCount:当有 Observer订阅源 Observable时,自动调用 connect,当 Observer全部 unsubscribe后,即没有 Observer了,自动调用 connect().unsubscribe()退订。

const source = interval(500);
const refCounted = source.pipe(
  multicast(new Subject()),
  refCount()
);
const subscriptionA = refCounted.subscribe(v => console.log('A: ' + v));
const subscriptionB = refCounted.subscribe(v => console.log('B: ' + v));

setTimeout(() => {
  subscriptionA.unsubscribe();
  subscriptionB.unsubscribe();
}, 3000);
publish

multicast(new Subject())这段代码很常用,可用 publish将其简化

const refCounted = source.pipe(
  multicast(new Subject()),
  refCount()
);  // 等价于 👇
const refCounted = source.pipe(publish(), refCount());
share

publish(), refCount() 可以通过 share 将其简化

const refCounted = source.pipe(publish(), refCount()); // 等价于 👇
const shared = source.pipe(share());

错误处理操作符

# catch / catchError

捕获 rxjs 错误

import { of, throwError } from "rxjs";
import { catchError } from "rxjs/operators";

const sourceError = throwError("This is an error!");
const exampleError = sourceError.pipe(
  catchError((val) => of(`I caught: ${val}`))
);
exampleError.subscribe((val) => {
  console.log(`exampleError`, val);
});

image.png

retry

如果发生错误,以指定次数重试 observable 序列

const sourceRetry = interval(1000);
const exampleRetry = sourceRetry.pipe(
  mergeMap((val) => {
    if (val > 5) {
      return throwError("Error!");
    }

    return of(val);
  }),
  retry(2)
);

exampleRetry.subscribe({
  next: (val) => console.log(val),
  error: (val) => console.log(`get Error: ${val}`),
});

image.png

retryWhen

当发生错误时,基于自定义的标准来重试 observable 序列

const source = interval(1000);
const example = source.pipe(
  map((val) => {
    if (val > 5) {
      throw val;
    }
    return val;
  }),
  retryWhen((errors) =>
    errors.pipe(
      tap((val) => console.log(`Error: ${val}`)),
      // 5秒后重启
      delayWhen((val) => timer(val * 1000))
    )
  )
);

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

image.png

工具操作符

toPromise

将 Observable 转化为 Promise

import { of } from "rxjs";

of(5)
  .toPromise()
  .then((data) => {
    console.log(`toPromise`, data); // 输出5
  });

toArray

将 Observable 的值转化为数组

interval(500)
  .pipe(take(5), toArray())
  .subscribe((data) => {
    console.log(`toArray`, data); // 👉 2.5s 后输出 [ 0, 1, 2, 3, 4 ]
  });
delay

delay 延迟 n 秒输出

of(2)
  .pipe(delay(2000))
  .subscribe((data) => {
    console.log(`延迟 2s 输出`, data);
  });
timeout

在指定的间隔内,如果不发出值就报错

function makeRequest(time: number) {
  return of("Request Complete!").pipe(delay(time));
}

of(4000, 3000, 2000)
  .pipe(
    concatMap((time) =>
      makeRequest(time).pipe(
        timeout(2500),
        catchError((err) => of(`Request Error ${err}`))
      )
    )
  )
  .subscribe((val) => {
    console.log(val);
  });

结果:

image.png

finalize

当 Observable 完成时调用函数。

function makeRequest(time: number) {
  return of("Request Complete!").pipe(delay(time));
}

of(4000, 3000, 2000)
  .pipe(
    concatMap((time) =>
      makeRequest(time).pipe(
        timeout(2500),
        catchError((err) => of(`Request Error ${err}`))
      )
    ),
    finalize(() => {
      console.log(`finalize`);
    })
  )
  .subscribe((val) => {
    console.log(val);
  });

结果:

image.png

条件和布尔操作符

every

如果完成时所有的值都能通过断言,那么发出 true,否则发出 false

of(1, 2, 3, 4, 5)
  .pipe(every((val: number) => val % 2 === 0))
  .subscribe((val) => {
    console.log(`val`, val); // false
  });

of(2, 4, 6, 8)
  .pipe(every((val: number) => val % 2 === 0))
  .subscribe((val) => {
    console.log(`val`, val); // true
  });
find

只发出源发出的满足某些条件的第一个可观察值。


of(1, 5, 10)
  .pipe(find((val: number) => val % 5 === 0))
  .subscribe((val) => {
    console.log(`find ===>`, val); // 输出 5
  });
findIndex

只发出由满足某些条件的可观测源发出的第一个值的索引。

of(1, 5, 10)
  .pipe(findIndex((val: number) => val % 5 === 0))
  .subscribe((val) => {
    console.log(`find ===>`, val); // 输出 1
  });
isEmpty

如果 Observable 在完成之前没有发出任何值,返回 true, 否则返回 false。

const source = new Subject<string>();
const result = source.pipe(isEmpty());

source.subscribe((x) => console.log(x));
result.subscribe((x) => console.log(x));

source.next("a");
source.next("b");
source.next("c");
source.complete();

结果:

image.png

defaultIfEmpty

如果在完成前没有发出任何通知,那么发出给定的值

import { EMPTY, of } from "rxjs";
import { defaultIfEmpty, every } from "rxjs/operators";
EMPTY.pipe(defaultIfEmpty("defaultValue")).subscribe((val) => {
  console.log(val); // 输出 defaultValue
});

总结

粗略地总结了 rxjs 的一些基本使用,对于 rxjs 的介绍,这篇文章并不能完全覆盖到,后面会不断的更新,总结常见的 rxjs 用法和项目中的实战案例。

参考文章