前言
很长一段时间没有写 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.
Why Stream? Because... 管理
为什么要用 "stream" 概念去理解这些 "值" ? 不能简单的理解为 "点击" > "更新 value" ?
当然可以, 其实 stream 概念并不是为了理解, 而是为了管理.
当程序里出现越来越多, 变来变去的值以后, 出现 bug 的机率就越来越高, 而追踪 bug 也越来越吃力.
所以就必须整一套概念来管理它们. 这就好比你用 Redux 来管理 React 的 state 一样.
以前有许多人拿 redux 去管理简单的程序, 结果就是大材小用, 反而是 redux 本身增加了整个系统的复杂度...幸好后来出现了 hook 才把这群人拉了出来...(永远记得, 软件开发一定要看清楚当前项目需求, 选择合适的方案而不是最牛逼的方案)
观察者模式
上面提到了, 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 });