RxJS 难在哪里?

2,888 阅读7分钟

一、开始之前

本文适合初学 RxJS 者,有项目实践,有理论说明

二、项目示例: 使用 RxJS 打通前后端数据流

实践优先,以下基于 RxJS + Vite + 原生 JSExpress全栈项目

1、初始化项目

pnpx create vite rx-start-handle # 选择原生 js

pnpm add rxjs cors # 可能会遇到跨域问题

2、基于 RxJScount 的加减操作

说明
前端展示数据/发起请求
后端数据传输/计算

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、效果

VeryCapture_20230826210851.gif

三、为什么 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会在被订阅时“回放”先前的多个值给观察者,可以指定回放的数量。
AsyncSubjectObservable 完成时,只发出最后一个值给观察者。如果 Observable 没有完成,它将不会发出任何值。

九、项目适合 rxjs 吗?

  • 普通项目不用 RxJS 就可以解决大部分问题。
  • RxJS 提供强大的可观察对象的,操作符,处理管道中数据的能力,明显适合复杂的项目,统一项目数据管理,例如在 AngularNestJS 中就内置了 RxJS
  • 需要一段时间的学习过程,并且到熟练程度难度相对还比较高,因为相当于重新熟练一种编程范式。
类型说明
复杂异步RxJS 有自己异步处理方式,如果异步非常复杂,可以考虑使用 RxJS
复杂的状态管理如果你的代码里面复杂的状态,并与异步一起结合,也可以考虑使用 RxJS
实时同步使用 websocket 应用程序的具有实时特性的应用程序,可以考虑使用 RxJS
其他...

十、仅仅将 RxJs 作为统一数据层?

  • 将不同的数据来源,统一使用 RxJS 可观察对象进行处理。
    • HTTP 请求
    • WebSocket
    • 用户输入
  • 数据变换
    • 变换过滤映射等操作
  • 状态管理
    • 依托强大的数据操作能力,做状态里游刃有余
  • 组件通信
    • Subjects 充当中间介质,跨域组件通信
  • 异步操作
    • RxJS 支持异步并发,功能强大,容易组合
  • 错误处理
    • RxJS 的错误处理机制可以更好地处理异步操作可能出现的错误,从而使应用程序更加健壮

十一、小结

本基于 RxJSVite 的原生 JS + Express 的前后端作为实践的开始,然后讲解了 RxJS 的基本原理,通过更多的练习加强 RxJS 的能力,掌握 RxJS 需要大量不同场景的实践。RxJS 构建一套自己编程方式。同时分析 RxJS 是否适合自己的项目,以及是否适合作为统一的数据层处理数据。希望这篇文章能帮助阅读者。