ReactiveX 入门和应用:一切都是流

1,250 阅读5分钟

背景

本来计划分享的主题本来是reactive programming,即一种编程范式。因关于编程范式想讲的内容过多,本次先就其中一个具体实现,即ReactiveX入手,后面有机会可以再展开。

ReactiveX包含各个语言的实现,其中概念基本一致,具体的介绍中会以rxjs为例。

为什么要用

ReactiveX用来处理各种数据流,其中流是按顺序发生的离散事件,比如点击事件,比如网络请求回调,比如定时器回调等。有了这个工具我们处理异步事件流就像处理普通数组一样简单,从而简化了代码、可读性更强以及提高代码稳定性

** 注意响应式编程里一切都是流,要想应用这种思想,需要先把普通数据转化为流。**

前端为了处理异步,虽然引入了 Promise,乃至 async/await 的语法糖,但是对一些场景处理起来仍然很繁琐,比如竞态,由于切换 tab 或者下拉框发出多个请求,先发生的请求后返回就会造成页面数据渲染错误 request[1].png 利用现有解决方案,需要取消无效且未完成的请求或者将请求标记和结果匹配,比如使用 AbortController记录前一个请求,当发起新请求时将上一个请求取消。

再比如拖拽等复杂的事件处理。

具体介绍

ReactiveX为了解决数据流,提供了一个核心类型Observable以及其他相关类型和方法。

其中 Observable 表示一个数据流的生产者,可以被 Observer 使用 subscribe 监听,Observer是一个数据流的消费者,包含对数据不同情况(数据正常到达 next、数据流完成 complete 和出错 error)的回调,默认表示 next。

当数据流产生新数据时会执行 Observer 中定义的回调,这点类似于观察者模式(比如dom事件监听),不过和观察者模式不同的是,只有被监听的 Observable 才会开始执行,这一点类似于函数。

import { Observable } from "rxjs";

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

console.log("just before subscribe");
observable.subscribe({
  next(x) {
    console.log("got value " + x);
  },
  error(err) {
    console.error("something wrong occurred: " + err);
  },
  complete() {
    console.log("done");
  },
});
console.log("just after subscribe");

//Logs
//just before subscribe
//got value 1
//got value 2
//got value 3
//just after subscribe
//got value 4
//done

Observable实例调用subscribe后返回一个subscription,也称作Disposable,可以用来取消监听。

以上出现的数据源都是单播的,为了使用广播我们可以使用Subject,它

比如

import { Subject, from } from 'rxjs';

const subject = new Subject<number>();

subject.subscribe({
  next: (v) => console.log(`observerA: ${v}`)
});
subject.subscribe({
  next: (v) => console.log(`observerB: ${v}`)
});

const observable = from([1, 2, 3]);

observable.subscribe(subject); // You can subscribe providing a Subject

// Logs:
// observerA: 1
// observerB: 1
// observerA: 2
// observerB: 2
// observerA: 3
// observerB: 3

如果想对数据流的发生时机更细的控制,可以显式引入Scheduler。 .

Observable

Observabletc39处于提案阶段,有望被 js 标准化 (目前进展不大)。

为了更好的理解Observable,我们将其与其他概念做一下对比。

单值多值
Pull(同步)FunctionIterator
push (异步)PromiseObservable

pushpull是两个表述数据生产者和消费者通信的协议,如果生产者决定数据发送的时机,即 push,否则就是 pull,实质上也可以用异步和同步来表示。

不过 Observable 不仅仅能处理异步,还能处理同步,不过不是很有必要。

Operators

前面介绍了数据流的生产和消费过程,但ReactiveX的强大作用需要一系列 operators 来完成。 operators 是一系列函数,包括两类

  • Creation Operators 用来创建 Observable,比如将数组或事件等转化为数据流
import { from } from "rxjs";

const array = [10, 20, 30];
const result = from(array);

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

// Logs:
// 10
// 20
// 30
import { fromEvent } from "rxjs";

const clicks = fromEvent(document, "click");
clicks.subscribe((x) => console.log(x));

// Results in:
// MouseEvent object logged to console every time a click
// occurs on the document.
  • Pipeable Operators 即函数式编程中的纯函数,输入一个 Observable,生成一个新的 Observable,用来以旧的流为基础生成新的流,语法是observableInstance.pipe(operator()),如
import { of } from "rxjs";
import { map } from "rxjs/operators";

of(1, 2, 3)
  .pipe(map((x) => x * x))
  .subscribe((v) => console.log(`value: ${v}`));

// Logs:
// value: 1
// value: 4
// value: 9

类似于

[1, 2, 3].map((x) => x * x);

多个 Operators 表示成

obs.pipe(op1(), op2(), op3(), op4());
//相当于
op4()(op3()(op2()(op1()(obs))));

通常使用表示 operators 的工作方式

--a---b-c---d---X---|->

a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline

更多 operator 的表示看这里

有什么应用

当使用ReactiveX时要记住一句话,一切皆流,要把一切数据源先转化成流的形式再来处理。

比如要解决竞态的问题,可以将每个请求作为一个流,当新的流到来时就将旧流抛弃,这时候应使用switchAll,比如下面,完整代码看这里

  const [text, setText] = useObservableState<
    PokeItem[],
    React.ChangeEvent<HTMLInputElement>
  >((e) =>
    e.pipe(
      pluckCurrentTargetValue,
      filter((val) => {
        console.log(val)
        return val.length > 0
      }),
      distinctUntilChanged(),
      switchMap((val) => from(getPokemonByName(val)))
    )
  )

另外一个场景是导出 excel,即使用不同页码参数依次去请求所有符合条件的数据,然后处理成 excel 导出所需要的格式进行下载。如果使用流的思路处理,即先生成所有请求所需要的参数数组,然后将对应的请求转化为流,等所有 Obversable 完成再进行格式转换实现导出,其中通过mergeAll设置最大并发量,即能保证下载速度,又能避免造成服务器过多压力,比如

//生成Obverable组成的数组
    const observables = new Array(restrict)
      .fill(0)
      .map((_, index) =>
        defer(() => getData(generateParams(index + 1)).unwrap())
      )
    //如果导出需要顺序,则mergeAll参数需要为1,否则导出顺序按照请求到达的先后顺序
    from(observables)
      .pipe(
        mergeAll(5),
        scan((pre, cur) => {
          return pre.concat(cur.list)
        }, []),
        last()
      )
      .subscribe({
        error() {},
        next(dataSource) {
          doDownload(firstRes.list.concat(dataSource))
        },
      })
  }

最后重复一遍reactiveX的使用要点:一切都是流。

参考