头图

rxjs 是一个强大的库,为我们提供了丰富的功能来处理异步数据流。在这些功能中,ReplaySubject 是一个非常有用的类,它在多种情况下表现突出。

ReplaySubjectSubject 的一种变体。与 Subject 类似,它是一个多播的 Observable,允许多个 Observer 订阅。然而,它有一个显著的不同点:它会缓存一定数量的值,并将这些值重新发射给所有新的订阅者。我们可以设定缓存的最大值数量或缓存的时间窗口。通过这种方式,ReplaySubject 使得新加入的订阅者能够接收到 Observable 中先前发射的值,即使这些值是在它们订阅之前发射的。

接下来通过详细分析其源代码来理解其工作原理。

rxjs 的源代码中,ReplaySubject 实际上是继承自 Subject 的:

class ReplaySubject<T> extends Subject<T> {
  private buffer: T[] = [];
  private bufferSize: number;
  private windowTime: number;
  private scheduler: SchedulerLike;

  constructor(bufferSize: number = Number.POSITIVE_INFINITY, windowTime: number = Number.POSITIVE_INFINITY, scheduler?: SchedulerLike) {
    super();
    this.bufferSize = bufferSize < 1 ? 1 : bufferSize;
    this.windowTime = windowTime < 1 ? 1 : windowTime;
    this.scheduler = scheduler;
  }
}

ReplaySubject 的构造函数接受三个参数:

  1. bufferSize:缓存的最大值数量,默认值为 Number.POSITIVE_INFINITY
  2. windowTime:缓存的时间窗口,默认值为 Number.POSITIVE_INFINITY,表示缓存的时间跨度是无限的。
  3. scheduler:可选的调度器,用于时间管理。

关键方法之一是 next 方法,它是用于发射一个值:

next(value?: T): void {
  const now = this.scheduler ? this.scheduler.now() : Date.now();
  this.buffer.push({ value, timestamp: now });

  this.trimBufferThenGetEvents();

  super.next(value);
}

这个方法除了调用 super.next(value) 以外,它还将值以及时间戳缓存到 buffer 中,并调用 trimBufferThenGetEvents 方法来维护缓存。

trimBufferThenGetEvents 方法是核心部分,它用来修剪缓存,确保它遵循 bufferSizewindowTime 的限制:

private trimBufferThenGetEvents() {
  const now = this.scheduler ? this.scheduler.now() : Date.now();
  const bufferSize = this.bufferSize;
  const windowTime = this.windowTime;
  const buffer = this.buffer;

  let events = buffer.slice();

  if (bufferSize < Number.POSITIVE_INFINITY) {
    events = events.slice(-(bufferSize));
  }

  if (windowTime < Number.POSITIVE_INFINITY) {
    events = events.filter(event => (now - event.timestamp) < windowTime);
  }

  this.buffer = events;
}

每次有新的值发射时,这个方法会对缓存进行修剪,确保其不超过设定的 bufferSizewindowTime

当订阅者订阅 ReplaySubject 时,他们会收到已经缓存的事件:

_subscribe(subscriber: Subscriber<T>): Subscription {
  const events = this.trimBufferThenGetEvents();
  for (let i = 0; i < events.length && !subscriber.closed; i++) {
    subscriber.next(events[i].value);
  }
  return super._subscribe(subscriber);
}

这个方法首先调用 trimBufferThenGetEvents 修剪缓冲区,然后将缓冲区中的所有事件发送给新订阅者。

下面是一个示例,展示 ReplaySubject 的基本用法:

import { ReplaySubject } from 'rxjs';

// 创建一个 ReplaySubject,缓存最近 2 个值
const replaySubject = new ReplaySubject(2);

// 订阅该 ReplaySubject
replaySubject.subscribe({
  next: (v) => console.log(`observerA: ${v}`)
});

// 发射 3 个值
replaySubject.next(1);
replaySubject.next(2);
replaySubject.next(3);

// 这里 observerB 会接收到最近的 2 个值 2 和 3
replaySubject.subscribe({
  next: (v) => console.log(`observerB: ${v}`)
});

// 依然会接收到值
replaySubject.next(4);

在这个例子中,ReplaySubject 缓存最近 2 个值。当 observerB 订阅时,它会接收到值 23。发射值 4 时,两个订阅者都接收到了。

ReplaySubject 的使用场景

  1. 缓存数据:当您希望新的订阅者能够立即接收到最新的数据(可能是在它们订阅之前发射的),可以使用 ReplaySubject。例如在实时聊天应用中,当新用户加入会话时,您可能希望他们看到最近的几条消息。
  2. 重复播放:在某些情况下,您希望重播某个数据序列。例如,音视频播放器的实现中,如果需要实现某种形式的回放功能,可以考虑采用 ReplaySubject 来缓存数据流并再现。
  3. 缓冲最新的信息:当某些数据流速特别快且订阅者可能会错过某些信息时,可以使用 ReplaySubject 缓存这些信息,使得订阅者不会错过重要的信息。
  4. 保存最近状态:在 Redux 或其他状态管理工具中,您可能想要保存最近的状态,以便在用户重新打开页面时能够恢复状态,可以使用 ReplaySubject 来实现保存和恢复状态的功能。

下一步,通过一个更复杂的例子来展示 ReplaySubject 的更多功能,这里演示一个场景,即显示最近推送的三条数据,并每隔一段时间推送新数据:

import { ReplaySubject } from 'rxjs';
import { interval } from 'rxjs';
import { take } from 'rxjs/operators';

// 创建一个 ReplaySubject,缓存最近 3 个值
const replaySubject = new ReplaySubject(3);

// 订阅该 ReplaySubject
replaySubject.subscribe({
  next: (v) => console.log(`observerA: ${v}`)
});

// 使用 interval 生成一个数字流,每隔 1 秒发射一个值,最多发射 5 个值
interval(1000).pipe(take(5)).subscribe(value => replaySubject.next(value));

// 延时 3 秒后再加入新订阅者
setTimeout(() => {
  replaySubject.subscribe({
    next: (v) => console.log(`observerB: ${v}`)
  });
}, 3000);

在这个例子中,observerA 会从 04 收到所有值。observerB 延时 3 秒之后订阅,由于 ReplaySubject 记住了最近的 3 个值,observerB 会收到值 2, 3, 4

综上所述,ReplaySubjectrxjs 中非常有用的工具,为我们提供了一种缓存和重播数据流的方式。了解它的工作原理和使用场景,及其源代码的分析,能让我们在实际项目中更好地利用它来解决复杂的异步处理问题。


注销
1k 声望1.6k 粉丝

invalid


引用和评论

0 条评论