2.10 Observables与RxJS


文档摘要

2.10 Observables 与 RxJS RxJS 是基于可观察序列的异步编程库,Angular 的 HTTP、表单状态流、路由参数流全部构建其上。本节是第 2 章的收口:把前几节反复出现的"流"讲透——冷与热、常用操作符、订阅生命周期管理。变更检测视角下,RxJS 是控制"变更发生频率与合并方式"的调度台:防抖、节流、切换、合并,全在这里完成。 前两节的状态仓库用到了 BehaviorSubject 与操作符,本节补齐底层语言,第 2 章至此十节闭环。 带着一个问题进入 带着一个问题进入:2.8 与 2.9 里反复出现的"订阅之后记得清理"到底在防什么?答案藏在流的机制里: 解释冷热可观察的区别,说明 HTTP 与 Subject 各属哪类。

2.10 Observables 与 RxJS

RxJS 是基于可观察序列的异步编程库,Angular 的 HTTP、表单状态流、路由参数流全部构建其上。本节是第 2 章的收口:把前几节反复出现的"流"讲透——冷与热、常用操作符、订阅生命周期管理。变更检测视角下,RxJS 是控制"变更发生频率与合并方式"的调度台:防抖、节流、切换、合并,全在这里完成。

前两节的状态仓库用到了 BehaviorSubject 与操作符,本节补齐底层语言,第 2 章至此十节闭环。

带着一个问题进入

带着一个问题进入:2.8 与 2.9 里反复出现的"订阅之后记得清理"到底在防什么?答案藏在流的机制里:

  1. 解释冷热可观察的区别,说明 HTTP 与 Subject 各属哪类。
  2. 熟练使用 map/filter/switchMap/debounceTime/takeUntil 五个高频操作符。
  3. 用 takeUntil 模式统一管理组件内所有订阅的销毁。
  4. 说明操作符链如何降低变更检测频率(合并高频变更为低频提交)。

一、冷与热:订阅语义

import { of, interval, Subject } from 'rxjs'; // 冷可观察:每个订阅者触发独立执行(HttpClient 请求即此类) const cold$ = interval(1000); // 两个订阅者各自从 0 数起,互不相干 // 热可观察:无论几个订阅者,执行只有一份,后进者错过已发射的值 const hot$ = new Subject<number>(); hot$.next(1); // 此刻无人订阅,值永久丢失 hot$.subscribe(v => console.log('A', v)); hot$.next(2); // 只有 A 收到 hot$.subscribe(v => console.log('B', v)); hot$.next(3); // A、B 都收到 // BehaviorSubject 是 Subject 的变体:新订阅者立即收到最近一个值

冷热之别解释了很多"灵异现象":同一个 HTTP 流订阅两次发出两次请求;晚订阅的组件拿不到早发射的值。2.9 节的 BehaviorSubject 仓库正是为了给晚进页面的组件补发当前值。

二、五个高频操作符

import { Subject, fromEvent } from 'rxjs'; import { map, filter, switchMap, debounceTime, takeUntil } from 'rxjs/operators'; export class SearchPanel { private api = inject(OrderApi); private destroy$ = new Subject<void>(); // 销毁信号源 constructor() { fromEvent<InputEvent>(this.inputEl(), 'input').pipe( map(e => (e.target as HTMLInputElement).value.trim()), filter(v => v.length >= 2), // 过滤:不合格的值到此为止 debounceTime(300), // 防抖:停顿 300ms 才放行 switchMap(v => this.api.list(v, 1)), // 切换:新值取消旧请求 takeUntil(this.destroy$) // 销毁信号一到,整条链终止 ).subscribe(page => { this.result = page.items; // 订阅回调属于异步事件,检查随后到来(第 2.8 节结论复用) }); } ngOnDestroy() { this.destroy$.next(); // 通知所有 takeUntil 停止 this.destroy$.complete(); } inputEl(): HTMLInputElement { return document.querySelector('#kw') as HTMLInputElement; } result: unknown[] = []; }

五个操作符一句话记忆:map 换值、filter 挡值、debounceTime 并发 Value合并成"最后一次"、switchMap 新旧二选一、takeUntil 统一断电。前四个在 2.4/2.7/2.8 节都以单点形式出现过,本节把它们串成一条链——真实项目的搜索框就是这条链。

三、订阅生命周期:泄漏从哪来

每个 subscribe 返回订阅对象,未取消的订阅在组件销毁后仍持有回调与组件引用。三类高危源:HTTP 流(未完成的会话流尤其)、fromEvent 绑定、Interval 类无限流。管理策略按代价排序:

  • async 管道:零成本,框架托管(首选);
  • takeUntil 模式:一个销毁信号断掉整组件所有订阅(次选);
  • 手动 add 到 Subscription 聚合:适合动态订阅集合。

💡 判断泄漏的土办法:组件销毁钩子里打日志,操作触发源(比如再点一次按钮)看控制台是否还有旧组件的输出——有就是泄漏。

图:操作符链对变更频率的整形

图:操作符链对变更频率的整形

四、案例:实时看板的数据流设计

背景:运维看板页有六个指标卡,每个卡各自轮询一个接口,间隔 5 秒。上线后发现 CPU 占用异常:六条轮询流各自触发检查,页面几乎每秒都在检查。
操作:合并轮询为单一节拍,一次拉取全部指标:

import { timer, forkJoin } from 'rxjs'; import { switchMap } from 'rxjs/operators'; metrics$ = timer(0, 5000).pipe( // 单节拍:立即执行一次,之后每 5 秒 switchMap(() => forkJoin({ // 汇聚六个请求为一个对象 cpu: this.api.cpu(), mem: this.api.memory(), disk: this.api.disk(), net: this.api.network(), qps: this.api.qps(), err: this.api.errors() })), takeUntil(this.destroy$) ); // 模板:六个卡片全部绑定 metrics$ | async 的字段

结果:检查频率从"每秒可能多次"降到"每 5 秒一次",且六卡数据同帧更新,消除了轮流闪跳。
解读:这是"调度台"论点的实证——检查次数不由数据量决定,而由变更提交的次数决定;把多次提交合并成一次,是继"降低每轮检查成本"(纯管道、trackBy)之外的第二条优化路线。两条路线将在第 3 章合流。
变式:指标重要性不同时用两条不同周期的节拍流(快线 5 秒、慢线 30 秒),仍然远好于六条独立流。

本节要点回顾

  • 冷热之别:HTTP 冷流多次订阅多次执行;Subject 热流后进错过,BehaviorSubject 补发最近值。
  • 五操作符:map/filter/debounceTime/switchMap/takeUntil 是日常主力,搜索框即完整样例。
  • 订阅纪律:async 管道 > takeUntil > 手动管理,无限流与事件流是泄漏重灾区。
  • 调度台:操作符链把高频变更合并为低频提交,直接减少检查轮数。
  • 第 2 章闭环:模块供依赖、组件当单元、绑定做采样、指令管道转形态、路由表单 HTTP 收输入、状态与流控频率——全册主线在下一章进入发动机舱。

第 2 章完。下一章拆开发动机:变更检测机制深度剖析。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U