一、开始之前
本文适合初学
RxJS
者,有项目实践,有理论说明
二、项目示例: 使用 RxJS
打通前后端数据流
实践优先,以下基于 RxJS
+ Vite
+ 原生 JS
和 Express
的全栈项目
。
1、初始化项目
pnpx create vite rx-start-handle # 选择原生 js
pnpm add rxjs cors # 可能会遇到跨域问题
2、基于 RxJS
的 count
的加减操作
端 | 说明 |
---|---|
前端 | 展示数据/发起请求 |
后端 | 数据传输/计算 |
3、Express
后端:三个 GET
接口
基于 RxJS
模拟异步操作使用 Promise
+ RxJS
,模拟数据库异步操作:
路由 | 说明 |
---|---|
/init | 初始化数据 |
/add | 数据 + n |
/dec | 数据 - n |
import express from 'express'
import cors from 'cors'
import { from } from 'rxjs'
const app = express();
let state = 0;
app.use(cors())
function op(action, timeout = 300, n = 1) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if(action === 'add') {
state += n
resolve(state)
} else if (action === 'dec') {
state -= n
resolve(state)
} else {
resolve(state)
}
}, timeout)
})
}
app.get('/init', (req, res) => {
const subscription = from(op('', 50, 1)).subscribe({
next: (v) => {
res.json({
code: 0,
message: 'success',
data: `${v}`
});
}
})
res.on('close', () => {
subscription?.unsubscribe()
})
})
app.get('/add', (req, res) => {
const subscription = from(op('add', 50, 1)).subscribe({
next: (v) => {
res.json({
code: 0,
message: 'success',
data: `${v}`
});
}
})
res.on('close', () => {
subscription?.unsubscribe()
})
})
app.get('/dec', (req, res) => {
const subscription = from(op('dec', 50, 1)).subscribe({
next: (v) => {
res.json({
code: 0,
message: 'success',
data: `${v}`
});
}
})
res.on('close', () => {
subscription?.unsubscribe()
})
})
app.listen(3000, () => {
console.log('port: http://localhost:3000')
})
模拟数据异步操作,不使用多播,不使用 Subject 主题。考虑到每一个请求都是新的, 都会创建一个新的可观察对象,为了内存不泄露,在请求结束之后取消了订阅。更加健壮的代码还需要根据自己的需求适当的处理。
4、对初始化的 vite 初始化的项目进行微调
import './styles/style.css'
import javascriptLogo from './assets/javascript.svg'
import viteLogo from '/vite.svg'
// js
import { handle } from './counter.js'
document.querySelector('#app').innerHTML = `
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
<img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
</a>
<h1>Hello Vite!</h1>
<div class="card">
<div id="result"></div>
<button id="add" type="button">server +</button>
<button id="dec" type="button">server -</button>
</div>
<p class="read-the-docs">
Click on the Vite logo to learn more
</p>
</div>
`
5、使用 RxJS 给按钮绑定可观察事件,并用函数式处理逻辑
import { fromEvent, switchMap, catchError } from 'rxjs'
import { addApi$, decApi$, initApi$ } from './api/request'
export function handle() {
const resultDiv = document.getElementById('result');
const setResultDivContent = (response) => {
if(response && response.code === 0) {
resultDiv.textContent = response.data
} else {
alert("更新数据失败")
}
}
const initSubscription = initApi$.subscribe({
next: (response) => {
setResultDivContent(response?.response)
},
error: () => { },
complete: () => {
console.log("completed!")
}
})
const addClick$ = fromEvent(document.querySelector('#add'), 'click')
const addSubscription = addClick$.pipe(
switchMap(() => addApi$),
catchError(error => {
console.error('An error occurred:', error);
return [];
})
).subscribe({
next: (response) => {
setResultDivContent(response?.response)
},
error: () => { },
complete: () => {
console.log("completed!")
}
})
const decClick$ = fromEvent(document.querySelector('#dec'), 'click')
const decSubscription = decClick$.pipe(
switchMap(() => decApi$),
catchError(error => {
console.error('An error occurred:', error);
return [];
})
).subscribe({
next: (response) => {
setResultDivContent(response?.response)
},
error: () => { },
complete: () => {
console.log("completed!")
}
})
// 如果需要在某个时机取消订阅,可以调用对应的unsubscribe方法
// initSubscription.unsubscribe();
// addSubscription.unsubscribe();
// decSubscription.unsubscribe();
}
在按钮上定义可观察对象,并通过 pipe
进行处理数据,使用 subscribe
订阅数据变化。我们看到以上的代码中,函数逻辑巨多,这是 RxJS
编程范式的特点(函数式编程)),同时需要注意的是,在合适时机取消订阅这些占用内存的 Observable
,如果使用框架一般是 组件销毁/副作用处理的返回值等
,本示例讲解了使用返回的 Subscription
对象的 unsubscribe
函数取消订阅
,在合适时机调用避免内存泄露, 尤其是单页应用。
6、效果
三、为什么 RxJS 看似简单,但实际上手难?
- 编程
范式
(函数、响应与常规的框架和库有较大区别。例如:在可观察对象中,DOM 事件
能与定时器
使用函数方式进行组合。) RxJS
可以理解为迭代器
与发布订阅模式
的结合体(实现推
数据,和多推
数据。)- 巨多的函数
操作符
需要理解与实践, 可观察对象自身的组合情况是繁多。
1、 如何破解难度?
- 确定对
RxJS
的需求度,一般的业务其实使用常规的JS
和框架就够完成任务了。 - 确定需要
RxJS
, 找到自己的合适的学习方式,不断的练习,可以在理论上归纳总结。 - 在不同的平台 Node.js、浏览器平台进行测试,
- 使用测试方式验证测试用例。
- 使用断点调试,调试源码。
四、RxJS 理论基础
名字 | 说明 | |
---|---|---|
Observer Pattern | 观察者模式 | 观察者和可观察对象,主题 |
Observable | 可观察对象 | 可观察对象表示一个异步的数据流或事件流 |
Observer | 观察对象 | 用于监听 Observable 对象的变化 |
Operators | 操作符 | 对可观察对象的进行操作,如果变换、映射、过滤等操作 |
Subscription | 订阅 | 订阅用于管理观察者和可观察对象之间的连接 |
Data Stream Control | 数据流控制 | 控制数据的流程频率等操作 |
Error Handling | 错误处理 | 对异常进行捕获和重试 |
Asynchronous Operations | 异步操作 | HTTP 请求、定时器 |
Parallel Operations | 并行操作 | 将多个可观察对象合并成一个 |
Memory Management | 内存管理 | 取消订阅,避免内存泄露 |
六、创建可观察对象的不同方式
创建可观察对象的不同方式 | 说明 |
---|---|
使用构造函数 | new 关键字 |
从操作符函数创建 | ajax/of/from/interval/... |
从操作符组合创建 | combineLatest/concat/... |
提示: 如何掌握如此之多的操作符?不断练习,找到常用操祖符,不断的练习,常用的 100% 会用。
七、异步特性
- RxJS 可以通过
bindCallback
操作符方便的将回调函数形式
转换成可观察对象的形式
。 - RxJS 可以通过
from
操作符方便的将Promise
转换成可观察对象的形式
。
以下是一些常见的 RxJS 异步操作符,以表格形式进行总结:
操作符 | 描述 | 示例 |
---|---|---|
debounceTime | 延迟发出值,只在停止输入一段时间后才发出 | debounceTime(300) |
throttleTime | 在一段时间内只发出第一个值 | throttleTime(300) |
delay | 延迟发出值 | delay(1000) |
timeout | 设置等待时间,超时后发出错误 | timeout(5000) |
八、从可观察对象到主题
前面已经提到了可观察对象,其实是 一对一
方式进行传播数据。基于观察者模式,能够扩展到 一对多
, 与从 可观察对象
到 主题
就出现了。
主题是一种特殊的 可观察对象
,主题自己实现了 next/complete/error
方法,自己能订阅和消费。但是在 RxJS 中主题根据不同的功能,可以分为以下四种:
主题种类名 | 说明 |
---|---|
Subject | 基本的主题,允许多个观察者订阅,并且可以通过调用 next() 方法来手动发出新值 |
BehaviorSubject | 当一个观察者订阅它时,它会立即发出最新的值,然后继续发出后续的值。它需要一个初始值作为参数。 |
ReplaySubject | 会在被订阅时“回放”先前的多个值给观察者,可以指定回放的数量。 |
AsyncSubject | 在 Observable 完成时,只发出最后一个值给观察者。如果 Observable 没有完成,它将不会发出任何值。 |
九、项目适合 rxjs 吗?
- 普通项目不用
RxJS
就可以解决大部分问题。 RxJS
提供强大的可观察对象的,操作符,处理管道中数据的能力,明显适合复杂的项目,统一项目数据管理,例如在Angular
和NestJS
中就内置了RxJS
。- 需要一段时间的学习过程,并且到熟练程度难度相对还比较高,因为相当于重新熟练一种编程范式。
类型 | 说明 |
---|---|
复杂 异步 | RxJS 有自己异步处理方式,如果异步非常复杂,可以考虑使用 RxJS |
复杂 的状态管理 | 如果你的代码里面复杂的状态,并与异步一起结合,也可以考虑使用 RxJS |
实时同步 | 使用 websocket 应用程序的具有实时特性的应用程序,可以考虑使用 RxJS |
其他 | ... |
十、仅仅将 RxJs
作为统一数据层?
- 将不同的数据来源,统一使用
RxJS
可观察对象进行处理。HTTP
请求WebSocket
- 用户
输入
- 数据变换
变换
、过滤
、映射
等操作
- 状态管理
- 依托强大的数据操作能力,做状态里游刃有余
- 组件通信
Subjects
充当中间介质,跨域组件通信
- 异步操作
RxJS
支持异步并发,功能强大,容易组合
- 错误处理
RxJS
的错误处理机制可以更好地处理异步操作可能出现的错误,从而使应用程序更加健壮
十一、小结
本基于 RxJS
、Vite
的原生 JS
+ Express
的前后端作为实践的开始,然后讲解了 RxJS
的基本原理,通过更多的练习加强 RxJS
的能力,掌握 RxJS
需要大量不同场景的实践。RxJS
构建一套自己编程方式。同时分析 RxJS
是否适合自己的项目,以及是否适合作为统一的数据层处理数据。希望这篇文章能帮助阅读者。