RxJS Observable 基础剖析:从 new Observable 构造函数到订阅、发射、完成与清理的全流程

导读

这段示例代码几乎把手工创建一个 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 functionsubscription 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$ 是惰性的,不会自动执行副作用。惰性特征是区分 ObservablePromise / 立即执行函数的常见切入口。(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 在生命周期末尾必须以 completeerror 之一终止;两者互斥,只能发生一次。终止后不允许再发射任何值;这被称为 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 终止时,完成回调触发。三种通知中任意一种终止型 (errorcomplete) 发生后,订阅关系自动结束,不再收到后续 next。(Telerik.com, rxjs.dev)

subscribe 返回一个 Subscription 对象;如果源流不会自动完成(例如 interval、DOM 事件流),你可以在适当时机调用其 unsubscribe() 主动清理,避免资源泄露或内存增长。在 Angular 组件中常在 ngOnDestroy 中退订,或使用 async 管道、takeUntiltakefirstValueFrom 等模式自动管理。(rxjs.dev, Telerik.com)


这段代码从执行到终止究竟发生了什么?

把上述行级信息串起来,可以得到一条完整生命周期轨迹。设想我们在浏览器控制台运行这段代码:

  1. 创建 observable$:此时只是定义一个惰性数据源;未执行内部函数体;没有任何副作用。惰性意味着多个订阅者会触发多次独立执行——典型冷流行为。(decodedfrontend.io, Angular University)

  2. 调用 observable$.subscribe(observerObj):rxjs 把你传入的 observerObj 包装成内部 Subscriber,然后调用在构造阶段提供的回调函数并把该 Subscriber 传入。(Telerik.com, Coding Scenes)

  3. 执行构造回调:打印日志、断点;接着同步依次调用 subscriber.next('数据1')subscriber.next('数据2');这些调用立即触发订阅端的 next 回调,两次日志输出。(Gist, rxjs.dev)

  4. 调用 subscriber.complete():源流宣告完成;rxjs 内部会标记终止、调用订阅端的 complete 回调、并执行清理 (teardown);随即 Subscription 状态变为关闭,后续再调用 subscriber.next() 将被忽略或抛出控制台警告 (视 rxjs 版本与调试模式而定)。(Telerik.com, rxjs.dev)

  5. 程序回到调用点;因为所有发射同步完成,subscribe 在返回前已经触发了全部通知;这与异步流截然不同,后者在未来的时间点才继续发射。(Gist, Telerik.com)


控制台期望输出顺序演示

若把上面代码单独放入脚本运行(假设浏览器控制台或 Node 环境),输出大致如下(中文日志略):

Observable 开始执行
what is a subscriber:  Subscriber { ... }
接收到数据: 数据1
接收到数据: 数据2
流完成

日志顺序验证了同步冷流的执行模型:订阅后立即执行生产者函数;发射值触发订阅端回调;完成通知终止流。(Gist, decodedfrontend.io)


同步与异步发射:为什么调试时会混淆?

本例是同步发射;在 subscribe 期间一次性发完所有值就完成。若改写为异步场景(如 setTimeoutinterval),next 通知会在事件循环的未来 tick 才出现;此时 subscribe 很快返回,后续值在计时器回调中持续发射。区分同步/异步十分重要,尤其在你尝试 subscription.unsubscribe() 时:对同步源而言常常已经来不及阻止本轮发射;对异步源则能及时停止未来发射。(Gist, Telerik.com)


冷流 (Cold Observable) 特性在本例中的体现

因为数据生产逻辑写在构造函数内,每个新订阅都会重新执行那段逻辑;因此多次订阅会得到独立的数据序列与副作用。这正是冷流定义:数据源与订阅者绑定,每个订阅拥有一条私有执行路径;相当于点播视频,每次点播从头播放,互不干扰。(decodedfrontend.io, Angular University)

若想共享同一执行(变热),可用 share()shareReplay()Subject 等手段将数据多播给多个订阅者;典型场景是浏览器事件或 WebSocket。(decodedfrontend.io, bitovi.com)


SubscriberObserver:你看到的参数究竟是谁?

示例里 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,但至多一次 errorcomplete;且两者互斥——一旦 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、表单值变化等。对不会自动完成的流,在组件销毁前退订十分关键;可借助 Subscriptionasync 管道或运算符控制生命周期。(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)

忘记退订长寿命流

intervalfromEvent 这类不会自然完成的流,如果不主动退订会持续占用资源;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 消息;这类流通常不自动完成,必须退订或转换;可借助清理函数、takeUntilasync 管道。(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 Observable 的骨架:惰性定义、订阅触发执行、通过 Subscriber 推送任意数量的数据通知、以错误或完成单次终止,并在终止时执行清理。把这些生命期节点与实际业务需求对应起来,就能写出更可控、更高性能的 Angular 响应式代码。祝你在构建复杂数据流时游刃有余。(Telerik.com, Angular)


(完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汪子熙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值