RxJS 系列 – 概念篇

前言

很长一段时间没有写 Angular 了 (哎...全栈的命),近期计划又要开始回去写了,于是就开始做复习咯。

我的复习是从 JS > TS > RxJS > Angular,与此同时当然是顺便写一系列半教程半复习的文章咯,我当年写了许多 Angular 的学习笔记,只是文笔太烂,这次得好好写了。

JS 已经复习到七七八八了,TS 老是提不起劲去写,所以就改成边写 TS 边写 RxJS 吧。

 

主要参考 

鐵人賽 – 30 天精通 RxJS 系列

鐵人賽 – 打通 RxJS 任督二脈 系列

 

以前写过相关的文章:

angular2 学习笔记 ( Rxjs, Promise, Async/Await 的区别 )

angular2 学习笔记 ( rxjs 流 )

 

什么是流 (stream) ?

RxJS 参杂了许多概念,什么函数式,观察者,异步等等。

但我个人觉得最容易理解的部分是 stream (流)。

stream 是什么?它表示一段时间内一个变化的状态。

在 JS 里,状态可以被理解为某个值,variable 的值。

时间则是用户使用 App 的时间。

看例子吧:

上图 (gif) 大概是 5 秒钟,这个就是时间,在这 5 秒中里面,价钱 (值) 变化了好几次 (160 -> 190 -> 200 -> 250) 

一个有时间,有变化的值就可以理解为一个 stream,所以价钱就是一个 Stream。

 

Why Stream? Because... 管理

为什么要用 "stream" 概念去理解这些 "值"?不能简单的理解为 "点击" -> "更新 value" ?

当然可以,其实 stream 概念并不是为了理解,而是为了管理。

当程序里出现越来越多,变来变去的值以后,出现 bug 的几率会越来越高,而追踪 bug 也越来越吃力。

所以就必须整一套概念来管理它们,这就好比你用 Redux 来管理 React 的 state 一样。

以前有许多人拿 redux 去管理简单的程序,结果就是大材小用,反而是 redux 本身增加了整个系统的复杂度...幸好后来出现了 hook 才把这群人拉了出来...(永远记得,软件开发一定要看清楚当前项目需求,选择合适的方案而不是最牛逼的方案)

 

Computed

上面提到了,stream 的其中一个特色就是变化。一个东西变化了,那么依赖它的东西通常也会跟着变化 -- 蝴蝶效应🦋

我们在写 Excel 的时候经常会写这样的逻辑 cell

 

full name 这个值,来自 first name + ' ' + last name,

而每当 first name 或 last name 变化以后,full name 也随之变化。

在上面这个例子里,first name, last name 就是 stream。随着时间它会发生变化。

而 full name 算是一个 depend and addon stream。它也会变化,同时它依赖其它的 stream 和一些额外的处理逻辑。

用 RxJS 来表达这类型的场景会非常贴切。

体验一下:

Without RxJS 实现:

const firstName = document.querySelector<HTMLInputElement>('#first-name')!;
const lastName = document.querySelector<HTMLInputElement>('#last-name')!;
const fullName = document.querySelector<HTMLSpanElement>('#full-name')!;

for (const input of [firstName, lastName]) {
  input.addEventListener('input', () => {
    fullName.textContent = `${firstName.value} ${lastName.value}`;
  });
}

用 RxJS 来实现:

const firstNameInput = document.querySelector<HTMLInputElement>('#first-name')!;
const lastNameInput = document.querySelector<HTMLInputElement>('#last-name')!;
const fullNameSpan = document.querySelector<HTMLSpanElement>('#full-name')!;

// 表达 stream
const firstName$ = fromEvent(firstNameInput, 'input').pipe(
  map(() => firstNameInput.value),
  startWith(firstNameInput.value)
);
const lastName$ = fromEvent(lastNameInput, 'input').pipe(
  map(() => lastNameInput.value),
  startWith(lastNameInput.value)
);
const fullName$ = combineLatest([firstName$, lastName$]).pipe(
  map(([firstName, lastName]) => `${firstName} ${lastName}`)
);

// 消费 stream
fullName$.subscribe(fullName => {
  fullNameSpan.textContent = fullName;
});

哇...怎么更复杂了...所以啊,上面说了,程序简单就没必要搞 RxJS 啊。

但你看看它的管理是不错的,表达 stream 负责描述 stream 的来源。

尤其是那个 combine stream 的表达尤其加分。

消费 stream 则可以做许多事情 (比如 render view)

这样 stream 可以被多个地方复用。

赠送一个优化版本:

// 这个可以封装起来
function fromInput(input: HTMLInputElement): Observable<string> {
  return fromEvent(input, 'input').pipe(
    map(() => input.value),
    startWith(input.value)
  );
}

// 表达 stream
const firstName$ = fromInput(document.querySelector<HTMLInputElement>('#first-name')!);
const lastName$ = fromInput(document.querySelector<HTMLInputElement>('#last-name')!);
const fullName$ = combineLatest([firstName$, lastName$]).pipe(
  map(([firstName, lastName]) => `${firstName} ${lastName}`)
);

// 消费 stream
const fullNameSpan = document.querySelector<HTMLSpanElement>('#full-name')!;
fullName$.subscribe(fullName => {
  fullNameSpan.textContent = fullName; // render view
});

 

Stream like a production line

Stream 通常指河流,但我觉得 RxJS Stream 更像是工厂里的生产线 / 流水线。

我们想象一间工厂的生产线长什么样。

  1. 生产线是一条长长的输送带
  2. 输送带旁边有 Operators 操作员 (或者 robot)
  3. 输送带上面有原材料、半成品、产品
  4. 流水线的源头是原材料,结尾是产品
  5. 流水线在运作的过程中,原材料从源头往结尾输送,它们会经过 Operators,
    Operators 会对原材料加工,变成半成品,然后再加工,变成最终的产品。
    往细节讲,Operators 还可能负责把次品选出来拿去 rework 等等不同的操作。

上面是 Overview,我们再细看它的流程。

  1. 生产线不是一开始就运作的,如果没有订单,生产线是不启动的,输送带也不会跑,输送带上也没有任何东西。
  2. 当订单来了,生产线开始运作,输送带开始跑。但是源头的原材料不一定马上就有,因为还得等供应商提供。
    当供应商供应原材料后,产品开始生产出货。
  3. 当订单完成或者被取消,生产线就关闭了。

RxJS 有几个基础步骤,大致上可以对应上面的各个场景。

  1. Observable
    Observable 就是一个生产线,它负责定义源头
    比如下面这句
    const firstNameInput = document.querySelector('input')!;
    const inputEvent$: Observable<Event> = fromEvent(firstNameInput, 'input');

    它的意思是创建了一个生产线,生产线的供应商是 input element 的 input event。
    当 input event dispatch 生产线就得到 input event 对象,这个就是原材料。

  2. Pipe
    Pipe 就是输送带,它没有实际意义,你可以把它理解为 Operators 的一个 container。
    const inputEvent$: Observable<Event> = fromEvent(firstNameInput, 'input').pipe();
  3. Operators
    Operators 就是操作员或 robot。
    const firstName$: Observable<string> = fromEvent(firstNameInput, 'input').pipe(
      map(e => (e.currentTarget as HTMLInputElement).value),
      filter(firstName => firstName !== '')
    );

    map 是一个 Operator,它负责把原材料 input event 加工变成半成品/产品 input value。
    filter 是一个 Operator,它负责过滤出合格的产品,比如 value !== '' 才算合格的产品,不合格的不可以交给买家。

  4. Subscribe
    subscribe 就是下订单
    const firstName$: Observable<string> = fromEvent(firstNameInput, 'input').pipe(
      map(() => firstNameInput.value),
      filter(firstName => firstName !== '')
    );
    firstName$.subscribe(firstName => console.log(firstName));

整个过程是这样发生的:

  1. 供应商是 input event listening
  2. 原材料是 input event
  3. map 操作员负责把原材料 input event 加工成产品 input value
  4. filter 操作员负责过滤出合格的产品 -- value !== ''
  5. 在下订单 (subscribe) 前,生产线 (Observable) 是停滞的,工厂也不会去跟供应商订货 (no yet input.addEventListener)
  6. 下订单后,工厂才开始想供应商要原材料 (input.addEventListener),此时生产线任然是空的,要等待供应商发货 (input dispatch event)
  7. 当原材料来了以后,经过 map operator 加工,filter operator 过滤次品,如果最终有产品就交付给买家。

 

Deferred Execution (延期执行)

上一 part 我们提到,如果没有人下订单 (subscribe),生产线 (Observable) 就是停滞的状态。

这个在 RxJS 被称为 Deferred Execution (延期执行)。

读历史的就知道,RxJS 是 C# LINQ 的衍生品,Deferred Execution 正是 LINQ 的特色之一。

const documentClicked$ = fromEvent(document, 'click');
setTimeout(() => {
  documentClicked$.subscribe(() => console.log('clicked'));
}, 1000);

fromEvent 是 document.addEventListener 的 RxJS 写法。

当 fromEvent 调用后,RxJS 并不会马上去 addEventListener。

而是等到 1 秒后 documentClicked$ stream 被 subscribe 后,才去 addEventListner。

这就是所谓的 Deferred Execution。

如果没有了 subscribe,所有 RxJS 都只是 declaration 而已。

 

Stream 与 Array 的关系

Stream 是一段时间内一个变化的状态,如果把每一次的改变放在一起看,那么它会长得像 Array。

let value = 1;
value = 2;
value = 3;
const value$ = [1, 2, 3];

Array 有 map, filter

Stream 也有 map, filter

因为这些都是对 value 的加工处理,这是它俩相像的地方。

它俩的主要区别在处理 value 的 timing。

[1, 2, 3].map(v => v + 1); // [2, 3 ,4]

Array 是静态的,一开始就有 [1, 2, 3] -> 然后 map -> 输出 [2, 3, 4] -> 结束。

Stream 是动态的,一开始是空,某个事件发布后 -> 有了 1 -> 经过 map 输出 2,此时还么结束。

又发布一个 2 -> 经过 map 输出 3 -> 又发布一个 3 -> 以此类推...

总结:它们的处理过程很像,只是 Stream 多了一个动态和时间的概念。

 

RxJS 与 Angular 的关系

Angular 为什么引入了 RxJS 概念?

其最大的原因就是为了实现 change detection,当 Model 改变的时候 View 需要被更新,这就是一个典型的观察者模式。

Angular 虽然使用 RxJS,但并没有很重,常见的地方只有 HttpClient、Router、Form。

我们在写 Angular Application 的时候也不需要强制自己去写 RxJS,适量的运用就可以了。

 

Observable vs Promise

两者区别还是挺大的:

  1. Promise 一定是异步,Observable 可能是同步,也可能是异步。
  2. Promise 只会发布一次。Observable 可能会发布多次。
  3. Observable 会延迟执行,Promise 会立刻执行。
  4. Observable 被 subscribe 多次会导致多次执行 (unitcast 概念),Promise 被 then 多次依然只会执行一次。
  5. 当 Observable 被立刻 subscribe 执行,同时它内部是一个异步发布,而且只发布一次,这个时候它和 Promise 最像,通常使用 Promise 会更恰当。

 

posted @ 2022-07-27 01:51  兴杰  阅读(130)  评论(0编辑  收藏  举报