Subject

480 阅读7分钟

Subject

什么是主题? RxJS Subject 是一种特殊类型的 Observable,它允许将值多播到许多观察者。虽然普通的 Observable 是单播的(每个订阅的 Observer 拥有一个独立的 Observable 执行),但主题是多播的。

一个 Subject 就像一个 Observable,但可以多播给许多观察者。主题就像 EventEmitters:它们维护着许多侦听器的注册表。

每个 Subject 都是一个 Observable。给定一个主题,您可以订阅它,提供一个观察者,它将开始正常接收值。从观察者的角度来看,它无法判断 Observable 的执行是来自一个普通的单播 Observable 还是一个 Subject。

在主题内部,订阅不会调用传递值的新执行。它只是在一个观察者列表中注册给定的观察者,类似于 addListener 通常在其他库和语言中的工作方式。

Every Subject is an Observer。它是一个具有 next(v)、error(e) 和 complete() 方法的对象。要向 Subject 提供一个新值,只需调用 next(theValue),它就会被多播给注册监听 Subject 的观察者。

在下面的例子中,我们有两个观察者附加到一个主题,我们为主题提供了一些值:


import { Subject } from 'rxjs';

const subject = new Subject<number>();

subject.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

subject.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

subject.next(1);

subject.next(2);

// Logs:

// observerA: 1

// observerB: 1

// observerA: 2

// observerB: 2

由于一个 Subject 是一个观察者,这也意味着你可以提供一个 Subject 作为订阅任何 Observable 的参数,如下例所示:


import { Subject, from } from 'rxjs';

const subject = new Subject<number>();

subject.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

subject.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

const observable = from([1, 2, 3]);

observable.subscribe(subject); // You can subscribe providing a Subject

// Logs:

// observerA: 1

// observerB: 1

// observerA: 2

// observerB: 2

// observerA: 3

// observerB: 3

通过上述方法,我们基本上只是通过 Subject 将单播 Observable 执行转换为多播。这演示了 Subjects 是如何将任何 Observable 执行共享给多个 Observers 的唯一方法。

主题类型还有一些特化:BehaviorSubject、ReplaySubject 和 AsyncSubject。

多播 Observable

“多播 Observable”通过可能有许多订阅者的 Subject 传递通知,而普通的“单播 Observable”只向单个 Observer 发送通知。

一个多播的 Observable 在幕后使用一个 Subject 来让多个 Observer 看到相同的 Observable 执行。

在幕后,这就是multicast多播操作符的工作方式:观察者订阅底层主题,主题订阅源 Observable。下面的例子类似于前面使用 observable.subscribe(subject) 的例子:


import { from, Subject } from 'rxjs';

import { multicast } from 'rxjs/operators';

const source = from([1, 2, 3]);

const subject = new Subject();

const multicasted = source.pipe(multicast(subject));

// These are, under the hood, `subject.subscribe({...})`:

multicasted.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

multicasted.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

// This is, under the hood, `source.subscribe(subject)`:

multicasted.connect();

multicast返回一个 Observable,它看起来像一个普通的 Observable,但在订阅时就像一个 Subject。多播返回一个 ConnectableObservable,它只是一个带有 connect() 方法的 Observable。

connect() 方法对于确定共享 Observable 执行何时开始很重要。因为 connect() 在后台执行 source.subscribe(subject),connect() 返回一个 Subscription,您可以取消订阅以取消共享的 Observable 执行。

引用计数

手动调用 connect() 并处理订阅通常很麻烦。通常,我们希望第一个观察者到达时自动连接,并在最后一个观察者取消订阅时自动取消共享执行。

请考虑以下示例,其中发生此列表中概述的订阅:

1.第一个观察者订阅多播的 Observable

2.多播的 Observable 已连接

3.下一个值 0 传递给第一个观察者

4.第二个观察者订阅多播的 Observable

5.下一个值 1 传递给第一个观察者

6.下一个值 1 传递给第二个观察者\

7.First Observer 取消订阅多播的 Observable

8.下一个值 2 传递给第二个观察者

9.第二个观察者取消订阅多播的 Observable

10.与多播 Observable 的连接被取消订阅

为了通过显式调用 connect() 来实现这一点,我们编写了以下代码:


import { interval, Subject } from 'rxjs';

import { multicast } from 'rxjs/operators';

const source = interval(500);

const subject = new Subject();

const multicasted = source.pipe(multicast(subject));

let subscription1, subscription2, subscriptionConnect;

subscription1 = multicasted.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

// We should call `connect()` here, because the first

// subscriber to `multicasted` is interested in consuming values

subscriptionConnect = multicasted.connect();

setTimeout(() => {

subscription2 = multicasted.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

}, 600);

setTimeout(() => {

subscription1.unsubscribe();

}, 1200);

// We should unsubscribe the shared Observable execution here,

// because `multicasted` would have no more subscribers after this

setTimeout(() => {

subscription2.unsubscribe();

subscriptionConnect.unsubscribe(); // for the shared Observable execution

}, 2000);

如果我们希望避免显式调用 connect(),我们可以使用 ConnectableObservable 的 refCount() 方法(引用计数),它返回一个 Observable 来跟踪它有多少订阅者。当订阅者数量从 0 增加到 1 时,它会为我们调用 connect(),从而开始共享执行。只有当订阅者数量从 1 减少到 0 时才会完全退订,停止进一步执行

refCount 使多播的 Observable 在第一个订阅者到达时自动开始执行,并在最后一个订阅者离开时停止执行。

下面是一个例子:


import { interval, Subject } from 'rxjs';

import { multicast, refCount } from 'rxjs/operators';

const source = interval(500);

const subject = new Subject();

const refCounted = source.pipe(multicast(subject), refCount());

let subscription1, subscription2;

// This calls `connect()`, because

// it is the first subscriber to `refCounted`

console.log('observerA subscribed');

subscription1 = refCounted.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

setTimeout(() => {

console.log('observerB subscribed');

subscription2 = refCounted.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

}, 600);

setTimeout(() => {

console.log('observerA unsubscribed');

subscription1.unsubscribe();

}, 1200);

// This is when the shared Observable execution will stop, because

// `refCounted` would have no more subscribers after this

setTimeout(() => {

console.log('observerB unsubscribed');

subscription2.unsubscribe();

}, 2000);

// Logs

// observerA subscribed

// observerA: 0

// observerB subscribed

// observerA: 1

// observerB: 1

// observerA unsubscribed

// observerB: 2

// observerB unsubscribed

refCount() 方法只存在于 ConnectableObservable 上,它返回一个 Observable,而不是另一个 ConnectableObservable。

BehaviorSubject 行为主体

Subjects 的变体之一是 BehaviorSubject,它具有“当前值”的概念。它存储发送给其消费者的最新值,每当有新的观察者订阅时,它将立即从 BehaviorSubject 接收“当前值”。

BehaviorSubjects 可用于表示“随时间变化的值”。例如,生日的事件流是一个主题,但一个人的年龄流将是一个行为主题。

在以下示例中,BehaviorSubject 被初始化为第一个 Observer 在订阅时收到的值 0。即使第二个观察者在发送值 2 之后订阅了它,它也会收到值 2。


import { BehaviorSubject } from 'rxjs';

const subject = new BehaviorSubject(0); // 0 is the initial value

subject.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

subject.next(1);

subject.next(2);

subject.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

subject.next(3);

// Logs

// observerA: 0

// observerA: 1

// observerA: 2

// observerB: 2

// observerA: 3

// observerB: 3

ReplaySubject 重播主题

ReplaySubject 与 BehaviorSubject 的相似之处在于它可以将旧值发送给新订阅者,但它也可以记录 Observable 执行的一部分。

ReplaySubject 从 Observable 执行中记录多个值,并将它们重播给新订阅者。

创建 ReplaySubject 时,您可以指定要重播的值数量:


import { ReplaySubject } from 'rxjs';

const subject = new ReplaySubject(3); // buffer 3 values for new subscribers

subject.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

subject.next(1);

subject.next(2);

subject.next(3);

subject.next(4);

subject.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

subject.next(5);

// Logs:

// observerA: 1

// observerA: 2

// observerA: 3

// observerA: 4

// observerB: 2

// observerB: 3

// observerB: 4

// observerA: 5

// observerB: 5

除了缓冲区大小之外,您还可以以毫秒为单位指定窗口时间,以确定记录的值可以保留多长时间。在以下示例中,我们使用 100 的大缓冲区大小,但窗口时间参数仅为 500 毫秒。


import { ReplaySubject } from 'rxjs';

const subject = new ReplaySubject(100, 500 /* windowTime */);

subject.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

let i = 1;

setInterval(() => subject.next(i++), 200);

setTimeout(() => {

subject.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

}, 1000);

// Logs

// observerA: 1

// observerA: 2

// observerA: 3

// observerA: 4

// observerA: 5

// observerB: 3

// observerB: 4

// observerB: 5

// observerA: 6

// observerB: 6

// ...

AsyncSubject 异步主题

AsyncSubject 是一个变体,其中只有 Observable 执行的最后一个值被发送给它的观察者,并且只有在执行完成时。


import { AsyncSubject } from 'rxjs';

const subject = new AsyncSubject();

subject.subscribe({

next: (v) => console.log(`observerA: ${v}`)

});

subject.next(1);

subject.next(2);

subject.next(3);

subject.next(4);

subject.subscribe({

next: (v) => console.log(`observerB: ${v}`)

});

subject.next(5);

subject.complete();

// Logs:

// observerA: 5

// observerB: 5

AsyncSubject 类似于 last() 运算符,因为它等待完整的通知以传递单个值。

Void subject 空题

有时发出的值与发出值的事实无关。 例如,下面的代码表示一秒钟已经过去。


const subject = new Subject<string>();

setTimeout(() => subject.next('dummy'), 1000);

以这种方式传递一个虚拟值很笨拙,可能会使用户感到困惑。 通过声明无效主题,您表示该值无关紧要。只有事件本身很重要。


const subject = new Subject<void>();

setTimeout(() => subject.next(), 1000);

带有上下文的完整示例如下所示:


import { Subject } from 'rxjs';

const subject = new Subject(); // Shorthand for Subject<void>

subject.subscribe({

next: () => console.log('One second has passed')

});

setTimeout(() => subject.next(), 1000);

共用API-asObservalbe

所有的Subject系列都有一个共用且常用到的API:asObservalbe,它的用途是将Subject当作Observalbe返回。这样有什么好处呢?由于Observable并没有next/complete/error这样的API,因此可以让得到这个Observable对象的程序专注在数据流的订阅相关的处理就好,不被允许发送新的事件。


class Student {

constructor() {

this._score$ = new Subject();

}

get score() {

return this._score$.asObservable();

}

updateScore(score) {

// 大于 70才允许推送

if (score > 70) {

this._score$.next(score);

}

}

}

const Jonny = new Student();

Jonny.score.subscribe((score) => {

console.log(`当前成绩:${score}`);

});

Jonny.updateScore(70);

Jonny.updateScore(50);

Jonny.updateScore(80); // 当前成绩: 80