RxJS 系列 – 概念篇
前言
很长一段时间没有写 Angular 了 (哎...全栈的命),近期计划又要开始回去写了,于是就开始做复习咯。
我的复习是从 JS > TS > RxJS > Angular,与此同时当然是顺便写一系列半教程半复习的文章咯,我当年写了许多 Angular 的学习笔记,只是文笔太烂,这次得好好写了。
JS 已经复习到七七八八了,TS 老是提不起劲去写,所以就改成边写 TS 边写 RxJS 吧。
主要参考
以前写过相关的文章:
angular2 学习笔记 ( Rxjs, Promise, Async/Await 的区别 )
什么是流 (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 更像是工厂里的生产线 / 流水线。
我们想象一间工厂的生产线长什么样。
- 生产线是一条长长的输送带
- 输送带旁边有 Operators 操作员 (或者 robot)
- 输送带上面有原材料、半成品、产品
- 流水线的源头是原材料,结尾是产品
- 流水线在运作的过程中,原材料从源头往结尾输送,它们会经过 Operators,
Operators 会对原材料加工,变成半成品,然后再加工,变成最终的产品。
往细节讲,Operators 还可能负责把次品选出来拿去 rework 等等不同的操作。
上面是 Overview,我们再细看它的流程。
- 生产线不是一开始就运作的,如果没有订单,生产线是不启动的,输送带也不会跑,输送带上也没有任何东西。
- 当订单来了,生产线开始运作,输送带开始跑。但是源头的原材料不一定马上就有,因为还得等供应商提供。
当供应商供应原材料后,产品开始生产出货。 - 当订单完成或者被取消,生产线就关闭了。
RxJS 有几个基础步骤,大致上可以对应上面的各个场景。
- Observable
Observable 就是一个生产线,它负责定义源头
比如下面这句
const firstNameInput = document.querySelector('input')!; const inputEvent$: Observable<Event> = fromEvent(firstNameInput, 'input');
它的意思是创建了一个生产线,生产线的供应商是 input element 的 input event。
当 input event dispatch 生产线就得到 input event 对象,这个就是原材料。 - Pipe
Pipe 就是输送带,它没有实际意义,你可以把它理解为 Operators 的一个 container。
const inputEvent$: Observable<Event> = fromEvent(firstNameInput, 'input').pipe();
- 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 !== '' 才算合格的产品,不合格的不可以交给买家。 - Subscribe
subscribe 就是下订单
const firstName$: Observable<string> = fromEvent(firstNameInput, 'input').pipe( map(() => firstNameInput.value), filter(firstName => firstName !== '') ); firstName$.subscribe(firstName => console.log(firstName));
整个过程是这样发生的:
- 供应商是 input event listening
- 原材料是 input event
- map 操作员负责把原材料 input event 加工成产品 input value
- filter 操作员负责过滤出合格的产品 -- value !== ''
- 在下订单 (subscribe) 前,生产线 (Observable) 是停滞的,工厂也不会去跟供应商订货 (no yet input.addEventListener)
- 下订单后,工厂才开始想供应商要原材料 (input.addEventListener),此时生产线任然是空的,要等待供应商发货 (input dispatch event)
- 当原材料来了以后,经过 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
两者区别还是挺大的:
- Promise 一定是异步,Observable 可能是同步,也可能是异步。
- Promise 只会发布一次。Observable 可能会发布多次。
- Observable 会延迟执行,Promise 会立刻执行。
- Observable 被 subscribe 多次会导致多次执行 (unitcast 概念),Promise 被 then 多次依然只会执行一次。
- 当 Observable 被立刻 subscribe 执行,同时它内部是一个异步发布,而且只发布一次,这个时候它和 Promise 最像,通常使用 Promise 会更恰当。