RxJS由浅入深——理解Subscription与Subscriber

114 阅读5分钟

前言

最近在巩固自己的知识体系,看到Rxjs的时候每次都很别扭,很容易忘记,接下来就详细的聊一下RxJS

响应式编程

RxJS离不开响应式编程,那么如何理解响应式编程呢?
概念: 响应式编程是一种基于数据流的范式编程,它强调“数据随时间推移的变化”,并通过观察者模式声明式管道(pipe) 处理异步事件。相比于传统的命令式编程,它避免了手动管理状态和回调地狱,通过RxJS使事件处理更加可组合和可维护。

例子

假设我们有一个按钮,点击后触发事件,并将按钮点击次数展示在页面上:
响应式编程

import { fromEvent } from 'rxjs';
import { map } from 'rxjs/operators';

const button = document.querySelector('button');
const clickStream = fromEvent(button, 'click');

clickStream.pipe(
  map((event, index) => index + 1) // 计算点击次数
).subscribe(count => {
  console.log(`按钮被点击了 ${count} 次`);
});

响应式编程的特点:

  1. 声明式:我们声明了数据流的行为,而不需要关心其具体的执行步骤。
  2. 组合性:通过管道(pipe)操作可以轻松地对事件流进行操作(例如:过滤、转换、合并等)。

命令式编程

let count = 0;
const button = document.querySelector('button');

button.addEventListener('click', function() {
  count++;
  console.log(`按钮被点击了 ${count} 次`);
});
  1. 逐步执行:需要显式地定义每个步骤(如何增加计数、如何展示等)。
  2. 回调函数:处理异步事件时,需要显式使用回调函数来管理每次事件的处理逻辑。
  3. 手动管理状态:状态变量(如 count)需要手动进行更新。

从类型文件中进行追溯

说回RxJS,这是很常见使用RxJS返回Observable来处理http请求

// 定义接口
public testHttp(): Observable<any> {
  const result = this.http.get('xxxxxxxxxxxxxxxxxxx', {});
  return result;
}
// 在具体的画面中调用接口
this.http.testHttp().subscribe((res: any) => {
    console.log('res',res);
});

这里涉及到两个概念Observablesubscribe
接下去我们在ide上点选到Observable的类型定义文件Observable.d.ts

image.png

可以看到Observable的构造函数接受了subscribe作为参数
subscribe为一个函数,提供了两个入参this和subscriber,而根据ts的语法this用于声明函数内部的 this 上下文类型,它不是实际的参数,所以存在一个subscriber的参数。

Subscription

而如果你点击subscriber的类型中查看会发现,subscriber 继承于 Subscription

image.png

即使在点击Subscription的定义中发现也没有在继承谁了,那么就从这开始说起

image.png

我们先简化一下Subscription结构并以interface的方式展示

interface Subscription {
    closed: boolean;              
    unsubscribe(): void;         
    add(teardown: TeardownLogic): void;    
    remove(teardown: TeardownLogic): void; 
}
  1. closed:表示订阅是否已关闭
  2. unsubscribe:表示取消订阅
  3. add:表示添加清理逻辑
  4. remove:表示移除清理逻辑

Subscription的本质是一个清除管理器

如何理解“清理管理器”?

用一个例子来解释,想象 Subscription 是一个退房清单管理器

// 创建一个退房清单管理器
const moveOutChecklist = new Subscription();

// 添加需要处理的事项
moveOutChecklist.add(() => {
    console.log('断水');
});

moveOutChecklist.add(() => {
    console.log('断电');
});

moveOutChecklist.add(() => {
    console.log('退还钥匙');
});

// 可以移除某个事项
const cleanRoom = () => console.log('打扫房间');
moveOutChecklist.add(cleanRoom);
moveOutChecklist.remove(cleanRoom); // 决定不打扫了

// 执行退房
moveOutChecklist.unsubscribe();
// 输出:
// 退还钥匙
// 断电
// 断水

这么看你会觉得,这更像是一个事务管理器,和清理(TearDown)有什么关系?放眼于整个Rxjs当中,他主要是用来处理资源的释放和清理。比如:

// 例如,当我们开启了一些资源
const subscription = new Subscription();

// 开启了一个定时器
const timer = setInterval(() => {
    console.log('tick');
}, 1000);

// 添加定时器的清理逻辑
subscription.add(() => {
    clearInterval(timer);  // 这就是清理操作
});

// 打开了一个文件
const file = openFile();  // 假设这是打开文件的操作
subscription.add(() => {
    file.close();  // 这是清理操作
});

// 当我们不再需要这些资源时
subscription.unsubscribe();  // 执行所有清理操作

作用

因此总结 Subscription的作用

  1. 管理清理函数的集合
const subscription = new Subscription();

// 添加清理函数
subscription.add(() => console.log('清理A'));
subscription.add(() => console.log('清理B'));

// 执行所有清理函数
subscription.unsubscribe();
// 输出:
// 清理B
// 清理A
  1. 提供统一的清理接口
class ResourceManager {
    private subscription = new Subscription();

    addCleanup(cleanup: () => void) {
        // 统一添加清理逻辑
        this.subscription.add(cleanup);
    }

    removeCleanup(cleanup: () => void) {
        // 移除特定的清理逻辑
        this.subscription.remove(cleanup);
    }

    cleanup() {
        // 统一执行所有清理
        this.subscription.unsubscribe();
    }
}
  1. 跟踪清理状态
const subscription = new Subscription();
console.log(subscription.closed); // false

subscription.unsubscribe();
console.log(subscription.closed); // true

Subscriber

在实际应用中接触Subscription可能少之又少,但是接触Subscriber就相对常见了 因为上面也提到了 Subscriber 继承的 Subscription,但它还实现了Observer

image.png

先说说Observer(观察者)Observer的interface相对简单很多

export interface Observer<T> {
    closed?: boolean;
    next: (value: T) => void;
    error: (err: any) => void;
    complete: () => void;
}

然后我们把ObserverSubscriptionSubscriber三者的结构进行简化整理得到

// Observer 接口定义
interface Observer<T> {
    next: (value: T) => void;
    error: (err: any) => void;
    complete: () => void;
}
// Subscription 的基本结构
interface Subscription {
    closed: boolean;
    unsubscribe(): void;
    add(teardown: TeardownLogic): void;
    remove(teardown: TeardownLogic): void;
}

// Subscriber 的基本结构(继承自 Subscription)
class Subscriber<T> extends Subscription implements Observer<T> {
    protected destination: Observer<T>;  // 新增:目标观察者
    closed: boolean;                     // 继承
    next(value: T): void;               // 新增:处理数据
    error(err: any): void;              // 新增:处理错误
    complete(): void;                   // 新增:处理完成
    unsubscribe(): void;                // 重写:除了清理还要处理状态
}

通过结构可以发现,Subscriber还具有处理数据流的能力(nexterrorcomplete),而这一切归功于实现了Observer。 那么也可以得到Subscriber的本质:清理管理器+观察者
这种设计使得 Subscriber 可以:

  1. 处理数据流:通过函数nexterrorcomplete
  2. 管理资源清理:通过函数addremoveunsubscribe
    在举一个例子加深一下大家的理解
// 创建 Observable
const observable = new Observable<number>(subscriber => {
    // subscriber 是 Subscriber 的实例
    // 1. 可以发送值
    subscriber.next(1);
    
    // 2. 可以添加清理逻辑
    const timer = setInterval(() => {
        subscriber.next(Math.random());
    }, 1000);
    
    // 添加清理逻辑
    subscriber.add(() => {
        clearInterval(timer);
    });
});

// 而普通的 Subscription 只能用于清理管理
const subscription = new Subscription();
subscription.add(() => console.log('清理'));

总结

文章主要介绍了响应式编程 Subscription以及 Subscriber 三个概念,看完之后你可能觉得还是云里雾里的很难理解,这是很正常的,RxJS响应式编程本身就是一个抽象的概念,不妨动手试试,只需要对着各个概念通过ide点进去查看相关的类型文件,再结合文章,回过头看看。