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 会更恰当。
RxJS 系列 – 概念篇的更多相关文章
- Google C++测试框架系列入门篇:第三章 基本概念
上一篇:Google C++测试框架系列入门篇:第二章 开始一个新项目 原始链接:Basic Concepts 词汇表 版本号:v_0.1 基本概念 使用GTest你肯定会接触到断言这个概念.断言是用 ...
- [ 高并发]Java高并发编程系列第二篇--线程同步
高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...
- iOS系列 基础篇 07 Action动作和输出口
iOS系列 基础篇 07 Action动作和输出口 目录: 1. 前言及案例说明 2. 什么是动作? 3. 什么是输出口? 4. 实战 5. 结尾 1. 前言及案例说明 上篇内容我们学习了标签和按钮 ...
- (Hibernate进阶)Hibernate系列——总结篇(九)
这篇博文是hibernate系列的最后一篇,既然是最后一篇,我们就应该进行一下从头到尾,整体上的总结,将这个系列的内容融会贯通. 概念 Hibernate是一个对象关系映射框架,当然从分层的角度看,我 ...
- 深入理解javascript函数系列第一篇——函数概述
× 目录 [1]定义 [2]返回值 [3]调用 前面的话 函数对任何一门语言来说都是一个核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即 ...
- 深入理解javascript函数系列第二篇——函数参数
× 目录 [1]arguments [2]内部属性 [3]函数重载[4]参数传递 前面的话 javascript函数的参数与大多数其他语言的函数的参数有所不同.函数不介意传递进来多少个参数,也不在乎传 ...
- 深入理解javascript作用域系列第二篇——词法作用域和动态作用域
× 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极 ...
- Java多线程系列--“基础篇”11之 生产消费者问题
概要 本章,会对“生产/消费者问题”进行讨论.涉及到的内容包括:1. 生产/消费者模型2. 生产/消费者实现 转载请注明出处:http://www.cnblogs.com/skywang12345/p ...
- Java多线程系列--“基础篇”04之 synchronized关键字
概要 本章,会对synchronized关键字进行介绍.涉及到的内容包括:1. synchronized原理2. synchronized基本规则3. synchronized方法 和 synchro ...
- Java多线程系列--“基础篇”02之 常用的实现多线程的两种方式
概要 本章,我们学习“常用的实现多线程的2种方式”:Thread 和 Runnable.之所以说是常用的,是因为通过还可以通过java.util.concurrent包中的线程池来实现多线程.关于线程 ...
随机推荐
- Vue源码剖析
目录 Vue 响应式数据 Vue 中如何进行依赖收集 Vue 中模板编译原理 Vue 生命周期钩子 Vue 组件 data 为什么必须是个函数? nextTick 原理 set 方法实现原理 虚拟 d ...
- Java 网络编程(TCP编程 和 UDP编程)
1. Java 网络编程(TCP编程 和 UDP编程) @ 目录 1. Java 网络编程(TCP编程 和 UDP编程) 2. 网络编程的概念 3. IP 地址 3.1 IP地址相关的:域名与DNS ...
- Vue 中引用第三方js总结
vue中引用第三方js总结 By:授客 QQ:1033553122 实践环境 win10 Vue 2.9.6 本文以引用jsmind为例,讲解怎么在vue中引用第三方js类库 基础示例 1.把下载好的 ...
- springboot3整合高版本spring data neo4j
本博客适用于springboo data neo4j 7.2.6版本,详情阅读官网https://docs.spring.io/spring-data/neo4j/reference/7.2/intr ...
- 【Oracle】Windiws-11G 安装
教程参考: https://jingyan.baidu.com/article/363872eccfb9266e4aa16f5d.html 安装包文件目录: 注意,使用[管理员运行此文件] 然后稍等许 ...
- 【ECharts】03 样式
ECharts4 开始,除了默认主题外,内置了两套主题,分别为 light 和 dark. 设置方式: var chart = echarts.init(dom, 'light'); var char ...
- 计算机领域:学术写作中的conducive的含义表示
"Conducive" 的意思是"有助于"或"有益于".在学术和正式的写作中,"conducive" 常用于描述某种情况 ...
- A* 算法、PathFinding问题中的 allow diagonal 和 don't cross corners,以及 .map文件格式(续)
前文: A* 算法.PathFinding问题中的 allow diagonal 和 don't cross corners,以及 .map文件格式 上篇讲了些关于地图文件 .map 的介绍,本文主要 ...
- faster-fifo:C++实现的python多进程通信队列 —— 强化学习ppo算法库sample-factory的C++实现的python多进程通信队列 —— python3.12版本下成功通过测试
项目地址: https://github.com/alex-petrenko/faster-fifo 需要注意,该项目给出了两种安装方法,一种是pip从pypi官网安装,一种是从GitHub上的源码安 ...
- 内网穿透之实践记录,使用花生壳进行内外穿透,场景:在家远程ssh连接到公司电脑或学校服务器
今天在网上闲逛的时候看到这样一个内网穿透的软件,ngrok, https://gitee.com/kxwinxp/ngrok 记得10多年前自己在读大学的时候曾经好一段时间在研究内网穿透技术,最后发现 ...