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包中的线程池来实现多线程.关于线程 ...
随机推荐
- 自动化车间3D可视化设计思路
自动化车间3D可视化设计思路 随着国内制造业企业的高速发展,再加上政策支持,高效的生产模式和先进的管理方式越来越受到企业重视.更多的企业将工业信息化技术进行广泛的应用,比如MES系统.数字孪生以及生产 ...
- Python爬虫(1-4)-基本概念、六个读取方法、下载(源代码、图片、视频 )、user-agent反爬
Python爬虫 一.爬虫相关概念介绍 1.什么是互联网爬虫 如果我们把互联网比作一张大的蜘蛛网,那一台计算机上的数据便是蜘蛛网上的一个猎物,而爬虫程序就是一只小蜘蛛,沿着蜘蛛网抓取自己想要的数据 解 ...
- Django 通过自定义context_processors实现自定义tag
通过自定义context_processors实现自定义tag by:授客 QQ:1033553122 测试环境 Win7 Django 1.11 实践 步骤1 应用根目录下,新建自定义context ...
- 题解:P10520 [XJTUPC2024] 榕树之心
题意 给予你 \(x\) 和 \(y\),将 \(x,y\) 代入. 前面的一大堆都无用. 思路 将题目中的公式代入即可. 代码 #include<bits/stdc++.h> using ...
- 【DataBase】MySQL 04 图形化用户界面管理工具
参考至视频:P16 - P18 https://www.bilibili.com/video/BV1xW411u7ax?p=82 SQL图形化界面管理工具 - SQLyog 随便找的一个下载地址[安装 ...
- 【RabbitMQ】09 深入部分P2 消费限流 & TTL
1.消费限流设置 就是设置项的2个调整,当然还有前面的手动确认的监听改动处理 https://www.bilibili.com/video/BV15k4y1k7Ep?p=26 2.消息过时设置 TTL ...
- 【Vue】05 Webpack
Webpack是一个现代JS应用的静态模块打包的工具 学习Webpack需要我们安装NodeJS 配置CNPM & CRM 使用切换镜像的方式配置:[不建议] npm config set r ...
- Apache DolphinScheduler社区又一PMC获推选通过!
PROFILE 姓名:程鑫 公司:阿里云 职位:开发工程师 Github ID: rickchengx 从事领域:大数据调度系统开发 兴趣爱好:健身 推举理由 他于2022年8月2日开始了他的Dolp ...
- CF1051F题解
The Shortest Statement 算法:树链剖分,最小生成树,最短路. 先讲一下题意:有一个 \(n\) 点 \(m\) 边的无向连通图,\(q\) 次询问,每次询问 \(a\) 到 \( ...
- 023.Ubuntu常见个性化配置
root登录设置 ubuntu默认关闭了root账户,可根据实际情况开启或关闭root登录. ubuntu@localhost:~$ sudo apt install openssh-server u ...