前言

很长一段时间没有写 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 会更恰当。

RxJS 系列 – 概念篇的更多相关文章

  1. Google C++测试框架系列入门篇:第三章 基本概念

    上一篇:Google C++测试框架系列入门篇:第二章 开始一个新项目 原始链接:Basic Concepts 词汇表 版本号:v_0.1 基本概念 使用GTest你肯定会接触到断言这个概念.断言是用 ...

  2. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

  3. iOS系列 基础篇 07 Action动作和输出口

    iOS系列 基础篇 07 Action动作和输出口 目录:  1. 前言及案例说明 2. 什么是动作? 3. 什么是输出口? 4. 实战 5. 结尾 1. 前言及案例说明 上篇内容我们学习了标签和按钮 ...

  4. (Hibernate进阶)Hibernate系列——总结篇(九)

    这篇博文是hibernate系列的最后一篇,既然是最后一篇,我们就应该进行一下从头到尾,整体上的总结,将这个系列的内容融会贯通. 概念 Hibernate是一个对象关系映射框架,当然从分层的角度看,我 ...

  5. 深入理解javascript函数系列第一篇——函数概述

    × 目录 [1]定义 [2]返回值 [3]调用 前面的话 函数对任何一门语言来说都是一个核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即 ...

  6. 深入理解javascript函数系列第二篇——函数参数

    × 目录 [1]arguments [2]内部属性 [3]函数重载[4]参数传递 前面的话 javascript函数的参数与大多数其他语言的函数的参数有所不同.函数不介意传递进来多少个参数,也不在乎传 ...

  7. 深入理解javascript作用域系列第二篇——词法作用域和动态作用域

    × 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极 ...

  8. Java多线程系列--“基础篇”11之 生产消费者问题

    概要 本章,会对“生产/消费者问题”进行讨论.涉及到的内容包括:1. 生产/消费者模型2. 生产/消费者实现 转载请注明出处:http://www.cnblogs.com/skywang12345/p ...

  9. Java多线程系列--“基础篇”04之 synchronized关键字

    概要 本章,会对synchronized关键字进行介绍.涉及到的内容包括:1. synchronized原理2. synchronized基本规则3. synchronized方法 和 synchro ...

  10. Java多线程系列--“基础篇”02之 常用的实现多线程的两种方式

    概要 本章,我们学习“常用的实现多线程的2种方式”:Thread 和 Runnable.之所以说是常用的,是因为通过还可以通过java.util.concurrent包中的线程池来实现多线程.关于线程 ...

随机推荐

  1. 各类配置文件(DNS, Firefox,Edge)

    DNS配置 腾讯DNS: 119.29.29.29 2402:4e00::  2402:4e00:1:: 阿里云: 223.5.5.5 223.6.6.6 2400:3200::1 2400:3200 ...

  2. Vue禁止用户复制文案 + 兼容 IE

    vue必须要加载完才可以操作dom,或者在mounted和created时使用this.$nextTick方法,使dom生成后进行相关操作. created() { this.$nextTick(() ...

  3. 【译】宣布三项新的高级 Visual Studio 订阅者福利

    Visual Studio 订阅(无论是专业版还是企业版)提供的不仅仅是软件使用权:这是一个全面的工具包,旨在显著提高您的开发能力和职业发展.这些订阅每年可以为您节省数千美元,提供各种服务,从每月用于 ...

  4. Linux安装软件命令详解

    Linux安装软件命令详解 目录 一.deb包的简介.安装及卸载步骤 二.rpm包的简介.安装及卸载步骤 三.AppImage包的简介.执行步骤 四.tar.gz.tar.bz2源代码包的简介.安装及 ...

  5. Fiddler关于https抓包

    一.Fiddler默认只抓取HTTP请求 Fiddler安装后默认只抓取HTTP请求,如要抓取HTTPS请求需要进行证书安装 二.Fiddler导出HTTPS证书 1.勾选HTTPS 工具栏Tools ...

  6. matplotlib中渐变颜色条转CSS样式(hex格式)——同mapbox中cog的颜色条拉伸显示

    matplotlib中渐变颜色条转CSS样式(hex格式)--同mapbox中cog的颜色条拉伸显示 应用场景: 1.适用于mapbox中显示cog影像时,colormap_name拉伸颜色条转换 2 ...

  7. python中不同方法的按索引读取数组的性能比较——哪种按索引读取数组的性能更好

    写python代码这么多年,从来也没有想过不同方式的读取python数组会有什么太大的性能差距,不过这段时间写代码突然发现这个差别还挺大,于是就多研究了一下. 本文研究的是使用不同方式来对python ...

  8. vue之组件的简单使用

    1.背景 2.组件的简单使用 <!DOCTYPE html> <html lang="en"> <head> <meta charset= ...

  9. [NOI2007] 项链工厂 题解

    前言 题目链接:洛谷:Hydro & bzoj. 题意简述 yzh 喜欢写 DS 题!你要维护一个环: 顺时针移动 \(k\) 位: 翻转 \(2 \sim n\): 交换 \(i\) 与 \ ...

  10. Jenkins部署架构概述

    1.Jenkins是什么 Jenkins是一个开源的.提供友好操作界面的持续集成(CI)工具,起源于Hudson,主要用于持续.自动的构建/测试软件项目.监控外部任务的运行. Jenkins用Java ...