导读
这段示例代码几乎把手工创建一个 rxjs Observable
时会遇到的关键节点都凑齐了:构造、订阅回调、同步发射多个值、完成通知、错误通知的替代路径、以及在订阅端分别处理 next
/ error
/ complete
。理解它,就能把 Observable
背后的执行模型、Subscriber
对象的角色、冷流 (cold) 特性、以及完成与退订的关系串起来。Angular
应用里无论是 HttpClient
、表单异步校验、事件流,还是自定义服务里的数据流,都沿用同一套契约。(Angular, Telerik.com)
原始代码(便于逐行讲解)
注意:示例中的字符串使用单引号,避免与文中对双引号替换规则冲突;并保留了注释,便于对照。
import { Observable } from 'rxjs';
const observable$ = new Observable((subscriber) => {
// 1\. 订阅阶段:当 subscribe() 被调用时执行
console.log('Observable 开始执行');
console.log('what is a subscriber: ', subscriber);
debugger;
// 2\. 发射阶段:发出数据
subscriber.next('数据1');
subscriber.next('数据2');
// 3\. 完成阶段:结束或错误
subscriber.complete(); // 或 subscriber.error(new Error('错误'))
});
observable$.subscribe({
next: (value) => {
console.log('接收到数据:', value); // 数据状态
},
error: (error) => {
console.log('发生错误:', error); // 错误状态
},
complete: () => {
console.log('流完成'); // 完成状态
},
});
行级拆解与逐句说明
下面按照代码自上而下拆解,同时穿插背景知识。每个片段后都标注它与 rxjs Observable
执行模型的联系。
import { Observable } from 'rxjs';
从 rxjs 包导入 Observable
类型构造器。rxjs 提供了一个类,用 new Observable(subscribeFn)
的形式创建自定义流。构造函数需要一个可执行的 subscribe
函数 (社区常称 observable function 或 subscription function),该函数在真正有订阅者时才被调用;这使得 Observable
天生惰性 (lazy)。(rxjs.dev, Telerik.com)
const observable$ = new Observable((subscriber) => { ... });
调用构造器并提供一个函数参数;rxjs 会把这里传入的回调保存起来,等 observable$.subscribe(...)
被调用时再执行。该回调收到的参数在类型层面是 Subscriber<T>
,它扩展了 Observer
,不仅暴露 next
/ error
/ complete
三个通知方法,还携带订阅管理与清理逻辑;在大多数示例中我们把它当成 observer
使用即可。(Coding Scenes, rxjs.dev)
注释:// 1\. 订阅阶段:当 subscribe() 被调用时执行
这句注释提示一个关键点:只有在有人订阅时,这个函数体里的代码才会运行;在此之前 observable$
是惰性的,不会自动执行副作用。惰性特征是区分 Observable
与 Promise
/ 立即执行函数的常见切入口。(Telerik.com, Angular University)
console.log('Observable 开始执行');
当真实订阅发生时,第一件事是你在构造函数里写的任意用户代码会立即运行。示例用日志标记执行起点,方便观察订阅触发的副作用。因为本例中所有发射都是同步的,你会看到这个日志在 subscribe
调用之后立即出现。同步与异步发射的行为差异在调试时很重要:同步流会在 subscribe
返回前就把所有值推送完。(Gist, Telerik.com)
console.log('what is a subscriber: ', subscriber);
输出传入的 subscriber
对象,帮助理解它的结构。这个对象实现了 rxjs 内部的 Subscriber
类,它包裹了调用方传入的 observer
,并保证发射安全性(例如防止在已完成后继续推送)。它也持有清理函数 (teardown) 的引用。(Coding Scenes, rxjs.dev)
debugger;
在浏览器 DevTools 中触发断点。因为 Observable
执行逻辑多发生在回调内部,放一个调试断点可以单步查看 subscriber
的属性、调用栈,确认何时进入构造回调。调试 Observable
的执行时序,是学习 rxjs 的有效手段;在 Angular
项目里同样适用。(Telerik.com, bitovi.com)
// 2\. 发射阶段:发出数据
表明接下来通过 subscriber.next(...)
推送值给订阅者。根据 Observable
契约,一个流可以发射零个、一个或多个值;每个值立刻送达所有当前订阅者的 next
回调。(rxjs.dev, bitovi.com)
subscriber.next('数据1');
与 subscriber.next('数据2');
向订阅者发送两条同步数据。由于这是在构造函数的同一同步宏任务里执行,订阅端的 next
回调会在 subscribe
调用栈仍未返回时依次触发;你会看到日志顺序严格对应发射顺序。rxjs 保证在错误或完成后不再调用 next
。(Telerik.com, rxjs.dev)
注释:// 3\. 完成阶段:结束或错误
Observable
在生命周期末尾必须以 complete
或 error
之一终止;两者互斥,只能发生一次。终止后不允许再发射任何值;这被称为 Observable
契约的一部分。(Angular University, Telerik.com)
subscriber.complete(); // 或 subscriber.error(new Error('错误'))
调用 complete
通知订阅端:流已无更多值。rxjs 在内部会自动调用关联的清理函数并解除订阅;因此对这种会自然完成的流,不强制要求手动退订。若改为 subscriber.error(...)
,则会发送错误通知并同样触发清理,但订阅端的 complete
回调不会执行;错误后流即终止。(Telerik.com, rxjs.dev)
订阅端对象逐项说明
observable$.subscribe({
next: (value) => {
console.log('接收到数据:', value); // 数据状态
},
error: (error) => {
console.log('发生错误:', error); // 错误状态
},
complete: () => {
console.log('流完成'); // 完成状态
},
});
subscribe
可接受一个完整的 Observer
对象;分别处理数据、错误、完成三种通知。任意省略的回调会被 rxjs 填补为空操作;社区自 v7 起鼓励使用对象形式而非多个独立回调参数,以提升可读性并规避弃用签名。(rxjs.dev, rxjs.dev)
当 next
被调用时,你获得发射值;当源流以 error
终止时,错误回调触发;当源流以 complete
终止时,完成回调触发。三种通知中任意一种终止型 (error
或 complete
) 发生后,订阅关系自动结束,不再收到后续 next
。(Telerik.com, rxjs.dev)
subscribe
返回一个 Subscription
对象;如果源流不会自动完成(例如 interval
、DOM 事件流),你可以在适当时机调用其 unsubscribe()
主动清理,避免资源泄露或内存增长。在 Angular
组件中常在 ngOnDestroy
中退订,或使用 async
管道、takeUntil
、take
、firstValueFrom
等模式自动管理。(rxjs.dev, Telerik.com)
这段代码从执行到终止究竟发生了什么?
把上述行级信息串起来,可以得到一条完整生命周期轨迹。设想我们在浏览器控制台运行这段代码:
-
创建
observable$
:此时只是定义一个惰性数据源;未执行内部函数体;没有任何副作用。惰性意味着多个订阅者会触发多次独立执行——典型冷流行为。(decodedfrontend.io, Angular University) -
调用
observable$.subscribe(observerObj)
:rxjs 把你传入的observerObj
包装成内部Subscriber
,然后调用在构造阶段提供的回调函数并把该Subscriber
传入。(Telerik.com, Coding Scenes) -
执行构造回调:打印日志、断点;接着同步依次调用
subscriber.next('数据1')
、subscriber.next('数据2')
;这些调用立即触发订阅端的next
回调,两次日志输出。(Gist, rxjs.dev) -
调用
subscriber.complete()
:源流宣告完成;rxjs 内部会标记终止、调用订阅端的complete
回调、并执行清理 (teardown);随即Subscription
状态变为关闭,后续再调用subscriber.next()
将被忽略或抛出控制台警告 (视 rxjs 版本与调试模式而定)。(Telerik.com, rxjs.dev) -
程序回到调用点;因为所有发射同步完成,
subscribe
在返回前已经触发了全部通知;这与异步流截然不同,后者在未来的时间点才继续发射。(Gist, Telerik.com)
控制台期望输出顺序演示
若把上面代码单独放入脚本运行(假设浏览器控制台或 Node 环境),输出大致如下(中文日志略):
Observable 开始执行
what is a subscriber: Subscriber { ... }
接收到数据: 数据1
接收到数据: 数据2
流完成
日志顺序验证了同步冷流的执行模型:订阅后立即执行生产者函数;发射值触发订阅端回调;完成通知终止流。(Gist, decodedfrontend.io)
同步与异步发射:为什么调试时会混淆?
本例是同步发射;在 subscribe
期间一次性发完所有值就完成。若改写为异步场景(如 setTimeout
、interval
),next
通知会在事件循环的未来 tick 才出现;此时 subscribe
很快返回,后续值在计时器回调中持续发射。区分同步/异步十分重要,尤其在你尝试 subscription.unsubscribe()
时:对同步源而言常常已经来不及阻止本轮发射;对异步源则能及时停止未来发射。(Gist, Telerik.com)
冷流 (Cold Observable) 特性在本例中的体现
因为数据生产逻辑写在构造函数内,每个新订阅都会重新执行那段逻辑;因此多次订阅会得到独立的数据序列与副作用。这正是冷流定义:数据源与订阅者绑定,每个订阅拥有一条私有执行路径;相当于点播视频,每次点播从头播放,互不干扰。(decodedfrontend.io, Angular University)
若想共享同一执行(变热),可用 share()
、shareReplay()
、Subject
等手段将数据多播给多个订阅者;典型场景是浏览器事件或 WebSocket。(decodedfrontend.io, bitovi.com)
Subscriber
与 Observer
:你看到的参数究竟是谁?
示例里 console.log('what is a subscriber: ', subscriber);
会打印一个对象,其原型链展示 rxjs 内部类 Subscriber
。社区常把传入构造器的这个参数称为 observer
,因为它至少实现了 next
/ error
/ complete
;但 Subscriber
还负责订阅生命周期、错误隔离、以及与 Subscription
的集成。了解区别有助于编写更健壮的自定义源,尤其是在需要访问 subscriber.closed
或调用 add()
附加清理逻辑时。(Coding Scenes, rxjs.dev)
终止信号:complete
vs error
vs 主动 unsubscribe
终止路径存在三类:生产者调用 complete
;生产者调用 error
;消费者主动 unsubscribe
。它们的效果不同:
-
complete
:通知所有订阅端完成;自动触发清理;之后不再发射值。(Telerik.com, rxjs.dev) -
error
:通知错误并结束;complete
不会再被调用;同样执行清理。(Telerik.com, rxjs.dev) -
unsubscribe
:由订阅端发起;停止接收未来值并执行清理;但不会触发complete
回调,因为这不是源主动完成。(rxjs.dev, Telerik.com)
理解差异后,你会在 Angular
组件销毁时更有把握地选择退订策略,避免等待永不完成的流导致内存泄露。(rxjs.dev, Angular)
Observable
契约中的一次性错误与终止
rxjs 遵循的 Observable
契约规定:一个流可以发射任意数量的 next
,但至多一次 error
或 complete
;且两者互斥——一旦 error
,不会再 complete
;已 complete
的流不会再 error
。这种一次性终止机制简化了错误处理模型,也贴近真实世界的数据源行为。(Angular University, Telerik.com)
实验 1:在完成后尝试继续发射
下面的变体演示在 complete
后调用 next
会被忽略;你可在调试台观察结果。
import { Observable } from 'rxjs';
const afterComplete$ = new Observable<string>((subscriber) => {
subscriber.next('A');
subscriber.complete();
// 下行不会触发订阅端 next
subscriber.next('B');
});
afterComplete$.subscribe({
next: v => console.log('afterComplete next:', v),
complete: () => console.log('afterComplete done'),
error: e => console.error('afterComplete error:', e),
});
B
不会被打印;这印证了完成后的流不再发射值。(rxjs.dev, Telerik.com)
实验 2:错误终止与完成终止的对比
import { Observable } from 'rxjs';
const errorThenComplete$ = new Observable<string>((subscriber) => {
subscriber.next('X');
subscriber.error(new Error('故意错误'));
// 下行即使写了也不会执行
subscriber.complete();
});
errorThenComplete$.subscribe({
next: v => console.log('errorThenComplete next:', v),
error: e => console.log('errorThenComplete error:', e.message),
complete: () => console.log('errorThenComplete done'),
});
可以观察到 complete
永远不会触发;因为错误已终止流。(Telerik.com, rxjs.dev)
实验 3:多次订阅,验证冷流行为
import { Observable } from 'rxjs';
const cold$ = new Observable<number>((subscriber) => {
const value = Math.random();
console.log('生产一个随机数:', value);
subscriber.next(value);
subscriber.complete();
});
cold$.subscribe(v => console.log('订阅者1 获得', v));
cold$.subscribe(v => console.log('订阅者2 获得', v));
cold$.subscribe(v => console.log('订阅者3 获得', v));
每次订阅都会在控制台看到 生产一个随机数
日志;三个订阅者得到的数值各不相同,说明生产逻辑被独立执行三次——典型冷流。(decodedfrontend.io, Angular University)
实验 4:把冷流升温,多播共享
import { Observable, share } from 'rxjs';
const warming$ = new Observable<number>((subscriber) => {
const value = Math.random();
console.log('只生产一次随机数:', value);
subscriber.next(value);
subscriber.complete();
}).pipe(share());
warming$.subscribe(v => console.log('S1', v));
warming$.subscribe(v => console.log('S2', v));
warming$.subscribe(v => console.log('S3', v));
通过 share()
把执行共享给多个订阅者,使得三个订阅端看到相同的数值;这模拟热流 (hot) 行为,类似多人同时观看直播。(decodedfrontend.io, bitovi.com)
在 Angular 组件里使用自定义 Observable
把示例嵌入 Angular
组件更贴近真实项目。下面是一个最小组件示例,组件初始化时订阅一个手工流,在销毁时无需退订,因为我们让它同步完成;随后演示一个无限计时流,并展示退订。
// demo.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, interval, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
@Component({
selector: 'app-demo',
template: `
<button (click)="start()">开始计时</button>
<div>计时值: {{tick}}</div>
`
})
export class DemoComponent implements OnInit, OnDestroy {
tick = 0;
private timerSub?: Subscription;
ngOnInit(): void {
const sync$ = new Observable<string>((subscriber) => {
console.log('sync$ 执行');
subscriber.next('hello');
subscriber.next('world');
subscriber.complete();
});
sync$.subscribe({
next: v => console.log('sync$ next', v),
complete: () => console.log('sync$ complete'),
});
}
start(): void {
// interval 不会自动完成,需要退订
this.timerSub = interval(1000)
.pipe(take(10)) // 10 次后自动完成;去掉 take 则需手动退订
.subscribe(v => {
this.tick = v;
if (v === 5 && this.timerSub) {
// 示例:主动退订早于 take 完成
this.timerSub.unsubscribe();
console.log('手动退订 timer');
}
});
}
ngOnDestroy(): void {
// 双保险:如果没退订且未完成,销毁时退订
this.timerSub?.unsubscribe();
}
}
Angular
官方指南强调:框架广泛使用 Observable
作为异步接口;你可以手工创建,也可以消费内建流,如 HttpClient
、表单值变化等。对不会自动完成的流,在组件销毁前退订十分关键;可借助 Subscription
、async
管道或运算符控制生命周期。(Angular, rxjs.dev)
典型调试技巧
-
在构造回调中放置
debugger;
或日志,确认何时执行数据生产;惰性流只在订阅后执行。(Telerik.com, Angular University) -
打印传入的
subscriber
,观察其内部状态如closed
,理解为什么在完成后再发射无效。(Coding Scenes, Telerik.com) -
对同步源与异步源分别测试,在
subscribe
前后打点比较输出顺序;可参考社区示例区分同步与异步行为。(Gist, bitovi.com) -
在需要共享源时使用多播运算符,避免重复昂贵副作用;冷转热的心智模型在调优性能时很有用。(decodedfrontend.io, bitovi.com)
常见误区与澄清
把 Observable 等同于 Stream?
流 (stream) 是抽象的值序列概念;Observable
是一种具体的程序化接口,用于创建、组合、订阅流并对其变化作出反应。两者相关但不等同;在 rxjs 与 Angular
社区的许多文章中,Observable
常被描述为 对一个数据流的句柄。(Angular University, bitovi.com)
以为 subscribe 会异步?
subscribe
能启动任意源;如果源是同步的 (如在构造回调中立刻调用 next
),通知会在当前调用栈内发生;这意味着在 subscribe
返回前就可能收到所有值。调试时若依赖订阅返回的 Subscription
去立即 unsubscribe()
,可能赶不上同步发射;需了解源特性。(Gist, Telerik.com)
忘记退订长寿命流
像 interval
、fromEvent
这类不会自然完成的流,如果不主动退订会持续占用资源;Subscription.unsubscribe()
或运算符如 takeUntil
是常用解法;框架层推荐在组件销毁时统一处理。(rxjs.dev, Angular)
误以为 complete 与 unsubscribe 相同
complete
是源通知;unsubscribe
是消费者动作;前者触发后者,但单独退订不会触发完成回调。写库代码时要区分,以免漏掉资源清理或 UI 状态更新。(Telerik.com, rxjs.dev)
更深入:给自定义 Observable 添加清理函数
构造函数返回一个函数即可注册 teardown;当订阅终止(无论是完成、错误、还是主动退订)时 rxjs 会调用它,用于清理计时器、取消事件监听、释放句柄等。
import { Observable } from 'rxjs';
function createInterval(ms: number): Observable<number> {
return new Observable<number>((subscriber) => {
let i = 0;
const handle = setInterval(() => {
subscriber.next(i++);
}, ms);
// 返回清理逻辑
return () => {
clearInterval(handle);
console.log('interval 清理完成');
};
});
}
const customInterval$ = createInterval(500);
const sub = customInterval$.subscribe({
next: v => console.log('customInterval tick', v),
complete: () => console.log('customInterval complete'),
});
// 2s 后退订
setTimeout(() => sub.unsubscribe(), 2000);
这种模式与 rxjs 内置 interval
行为类似;退订会自动执行清理函数,防止资源泄露。(Telerik.com, rxjs.dev)
小测:读输出推断源类型
若你看到以下输出:
生产一个随机数: 0.723
订阅者1 获得 0.723
订阅者2 获得 0.723
订阅者3 获得 0.723
你会判断源是热流或已被共享;因为只生产一次随机数却被多个订阅者接收。与冷流多次独立生产的对照可帮助快速定位是否重复执行昂贵请求。(decodedfrontend.io, bitovi.com)
与实际业务场景的关联
-
HTTP 请求:
Angular HttpClient
返回的Observable
默认是单发并完成;无需手工退订;适合一次性数据拉取。(Angular, rxjs.dev) -
表单异步校验:常用自定义
Observable
异步发射校验结果并完成;与本文示例模式高度相似;传入控件值、发射校验对象、完成。(Coding Scenes, Angular) -
持续事件流:如窗口大小变化、WebSocket 消息;这类流通常不自动完成,必须退订或转换;可借助清理函数、
takeUntil
、async
管道。(rxjs.dev, Telerik.com)
一段可直接跑的最小可复现示例 (StackBlitz / Node)
下面提供一个既能在浏览器 StackBlitz、也能在 Node (安装 rxjs) 跑起来的脚本。复制粘贴即可验证前文行为;输出顺序与生命周期都可观察。
// main.ts
import { Observable } from 'rxjs';
function demoBasic() {
const observable$ = new Observable<string>((subscriber) => {
console.log('Observable 开始执行');
console.log('what is a subscriber: ', subscriber);
subscriber.next('数据1');
subscriber.next('数据2');
// 切换这一行以测试错误终止
// subscriber.error(new Error('错误'));
subscriber.complete();
// 下面发射无效
subscriber.next('数据3');
});
const sub = observable$.subscribe({
next: (value) => {
console.log('接收到数据:', value);
},
error: (error) => {
console.log('发生错误:', error.message ?? error);
},
complete: () => {
console.log('流完成');
},
});
// 即便主动退订也无妨;流已完成
sub.unsubscribe();
}
demoBasic();
运行时你会看到完成后 数据3
未被接收;手动退订不会再次触发任何回调。你可以取消注释错误行,体验错误终止路径。(Telerik.com, rxjs.dev)
延伸阅读
若想继续深入,可参考以下资料,它们对 rxjs 的惰性执行、热冷流、订阅与退订机制、错误契约等均有详解:
-
RxJS 官方
Observer
指南,解释next
/error
/complete
回调。(rxjs.dev, rxjs.dev) -
Telerik
Angular Basics
系列对订阅执行、退订、unsubscribe
vscomplete
的细致解说。(Telerik.com, rxjs.dev) -
冷热流心智模型与多播实践,Decoded Frontend。(decodedfrontend.io, bitovi.com)
-
Angular University 关于流与
Observable
区别、惰性执行、FRP 思路的深入文章;并讨论错误处理契约。(Angular University, Angular University)
收尾
这段短小的代码让我们看清了 rxjs Observable
的骨架:惰性定义、订阅触发执行、通过 Subscriber
推送任意数量的数据通知、以错误或完成单次终止,并在终止时执行清理。把这些生命期节点与实际业务需求对应起来,就能写出更可控、更高性能的 Angular
响应式代码。祝你在构建复杂数据流时游刃有余。(Telerik.com, Angular)
(完)