在Angular中,Subject、BehaviorSubject和ReplaySubject之间的区别是什么?

257

我一直在试图理解这三个概念:

我想使用它们,并知道何时和为什么使用它们,使用它们的好处是什么,尽管我已经阅读了文档、观看了教程并搜索了Google,但我仍然无法理解。

那么它们的目的是什么?最好有一个实际的案例,甚至不需要编码。

我更希望得到清晰的解释,而不仅仅是“a+b => c,您已订阅......"

谢谢


1
已经有一个带有可观察行为主题的问题了;https://dev59.com/kVkS5IYBdhLWcg3ww45d,而重放主题的文档在我看来是清晰明了的https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/replaysubject.md。 - eko
这个答案中对 Rxjs 主题有相当全面的介绍,与 peeksilet 的答案非常补充。其中还包括关于终止后行为的重要细节,因此查看一下很有帮助。 - user3743222
8个回答

501

主要取决于行为和语义。使用:

  • Subject - 订阅者只会在订阅之后接收到已发出的值。问问自己,这是否是您想要的?订阅者是否需要了解先前的值?如果不需要,则可以使用此选项;否则,请选择其他选项。例如,在组件与组件之间通信时。假设您有一个组件,它会在按钮单击时为其他组件发布事件。您可以使用具有 subject 的服务进行通信。

  • BehaviorSubject - 最新值被缓存。订阅者将在初始订阅时获得最新值。此主题的语义是表示随时间变化的值。例如,已登录用户。初始用户可能是匿名用户。但是一旦用户登录,新值就是经过身份验证的用户状态。

    BehaviorSubject 使用初始值初始化。对于编码喜好,这有时很重要。例如,您将其用空值初始化。然后在订阅中,您需要执行空检查。也许可以接受,也可能很烦人。

  • ReplaySubject - 它可以缓存指定数量的发射。任何订阅者都将在订阅时获取所有缓存的值。何时需要此行为?老实说,我没有任何需要此行为的情况,除了以下情况:

    如果您使用缓冲区大小为 1 初始化 ReplaySubject ,则它实际上就像一个 BehaviorSubject 一样表现。最后一个值始终被缓存,因此它就像随时间变化的值。在这种情况下,就不需要像使用null初始化 BehaviorSubject 的情况那样进行 null 检查。在这种情况下,直到第一次发布之前,订阅者都不会收到任何值。

所以,你应该根据你期望的行为选择使用哪种(Subject)。大部分情况下,你可能想使用 BehaviorSubject,因为你真正想要表示的是“随着时间变化的值”的语义。但我个人认为,使用初始化为 1ReplaySubject 替代也没什么问题。

你需要避免使用普通的 Subject,当你实际上需要的是一些缓存行为。例如,你正在编写一个路由守卫或一个解析器。在该守卫中,你获取了一些数据并将其设置在服务的 Subject 中。然后在路由组件中,你订阅该服务主题,试图获取在守卫中发出的那个值。糟糕了,值在哪里?它已经被发出了,呃。使用“缓存”类型的 subject!

另请参见:


2
这是一个简单易懂的区别。当服务中的值改变且组件需要显示相应值时,BehaviorSubjects 或 Replay Subject 是解决方案。 - Saiyaff Farouk
3
谢谢!ReplaySubject并且缓冲区大小为1正是我所需要的。我有一个路由守卫需要这个值,但需要等待第一次发射。因此,BehaviorSubject不太适合,因为我不想要初始值(null也不行,因为我正在使用它来表示状态)。 - Andrew M.
4
具有1个缓存区的ReplaySubject与BehaviorSubject不同,因为ReplaySubject会阻塞订阅者等待第一个值,而BehaviorSubject在创建时需要一个初始值。通常,您希望按需懒惰地获取数据,并且不需要任何初始值。 - Pat Niemeyer
8
如果您使用大小为1的缓冲区初始化ReplaySubject,它实际上就像一个BehaviorSubject一样运作,并不完全正确。请查看这篇优秀的博客文章以获取这两者之间的差异。例如,如果您订阅一个已完成的BehaviorSubject,则您将不会收到最后一个值,但对于ReplaySubject(1),您将收到最后一个值。 - Wilt
7
回放功能的一个相对简单的例子是,在聊天室或游戏大厅场景中,你希望新加入的用户能够看到最近的10条消息。 - James
显示剩余9条评论

57
主题:订阅时,它总是获取在其订阅之后推送的数据,即先前推送的值不会被接收

const mySubject = new Rx.Subject();

mySubject.next(1);

const subscription1 = mySubject.subscribe(x => {
  console.log('From subscription 1:', x);
});

mySubject.next(2);

const subscription2 = mySubject.subscribe(x => {
  console.log('From subscription 2:', x);
});

mySubject.next(3);

subscription1.unsubscribe();

mySubject.next(4);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.12/Rx.min.js"></script>

使用这个例子,以下是将在控制台中打印的结果。
From subscription 1: 2  
From subscription 1: 3  
From subscription 2: 3  
From subscription 2: 4  

注意,到达较晚的订阅将错过已经推送到主题中的一些数据。
2. 回放主题:通过保留一个以前值的缓冲区来帮助,在新的订阅中发出这些值。
以下是回放主题的用法示例,其中保留并在新的订阅上发出 2 个先前值的缓冲区。

const mySubject = new Rx.ReplaySubject(2);

mySubject.next(1);
mySubject.next(2);
mySubject.next(3);
mySubject.next(4);

mySubject.subscribe(x => {
  console.log('From 1st sub:', x);
});

mySubject.next(5);

mySubject.subscribe(x => {
  console.log('From 2nd sub:', x);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.12/Rx.min.js"></script>

这是控制台上给我们的结果:
From 1st sub: 3
From 1st sub: 4
From 1st sub: 5
From 2nd sub: 4
From 2nd sub: 5

行为主题:与重播主题类似,但只会重新发出最后一个发出的值,或者如果之前没有发出任何值,则发出默认值:

const mySubject = new Rx.BehaviorSubject('Hey now!');

mySubject.subscribe(x => {
  console.log('From 1st sub:', x);
});

mySubject.next(5);

mySubject.subscribe(x => {
  console.log('From 2nd sub:', x);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.12/Rx.min.js"></script>

结果如下:
From 1st sub: Hey now!
From 1st sub: 5
From 2nd sub: 5

参考资料:https://alligator.io/rxjs/subjects/

参考文章真的很有帮助。 - Dan

39

这是一个关于不同可观察类型的方便总结,命名可能有点不直观哈哈.

  • Subject - 订阅者只能在订阅后获取发布的值。
  • BehaviorSubject - 新的订阅者会在订阅立即获得上一个已发布的值或初始值。
  • ReplaySubject - 新的订阅者会在订阅立即获得所有之前发布的值。

1-n个已发布的值?如果有2个已发布的值,ReplaySubject会产生-1个已发布的值吗? - Jason Cheng
@JasonCheng 不,它在订阅时检索所有先前发布的值,更新答案 :) - Ricky Boyce

29

最受欢迎的答案明显是错误的,声称:

"如果您使用大小为1的缓冲区初始化ReplaySubject,那么它实际上的行为就像一个BehaviorSubject一样"


这并不完全正确;请查看这篇伟大的博客文章,了解这两者之间的区别。例如,如果您订阅已完成的BehaviorSubject,则不会接收到最后一个值,但对于ReplaySubject(1),您将接收到最后一个值。

这是一个重要的差异,不应忽视:

const behavior = new BehaviorSubject(null);
const replay = new ReplaySubject(1);

behavior.skip(1).subscribe(v => console.log('BehaviorSubject:', v));
replay.subscribe(v => console.log('ReplaySubject:', v));

behavior.next(1);
behavior.next(2);
behavior.complete();
behavior.subscribe(v => console.log('Late B subscriber:', v));

replay.next(1);
replay.next(2);
replay.complete();
replay.subscribe(v => console.log('Late R subscriber:', v));

请查看此代码示例这里,该示例来自有关此主题的另一篇优秀博客文章


19

来自Randall Koutnik的书“使用RxJS构建反应式网站”:

Subject是一个强化版的可观察对象。在其核心部分,Subject与常规的可观察对象类似,但每个订阅都连接到同一源。此外,Subject也是观察者,并具有next、error和done方法,可以向所有订阅者同时发送数据。由于Subject是观察者,它们可以直接传递到subscribe调用中,并通过主题将来自原始可观察对象的所有事件发送给其订阅者。

我们可以使用ReplaySubject来跟踪历史记录。一个ReplaySubject记录最后n个事件并将它们回放到每个新的订阅者。例如,在聊天应用程序中,我们可以使用它来跟踪以前的聊天记录。

BehaviorSubjectReplaySubject的简化版本。 ReplaySubject存储任意数量的事件,而BehaviorSubject仅记录最新事件的值。每当BehaviorSubject记录新的订阅时,它会将最新的值以及任何传递的新值发送给订阅者。当处理单个状态单元(例如配置选项)时,BehaviorSubject非常有用。


8

正如一些帖子中提到的那样,被接受的答案是错误的,因为 BehaviorSubject != ReplaySubject(1),这不仅仅是编码风格的偏好。

在评论中经常提到“guards”,这也是我最经常发现使用回放主题的用例的地方。更具体地说,如果您有一个类似于take(1)的场景,并且您不仅想要获取初始值。

例如,请查看以下内容:

  ngOnInit() {
    const behaviorSubject = new BehaviorSubject<boolean>(null);
    const replaySubject = new ReplaySubject<boolean>(1);
    this.checkLoggedIn(behaviorSubject, 'behaviorSubject');
    this.checkLoggedIn(replaySubject, 'replaySubject');
    behaviorSubject.next(true);
    replaySubject.next(true);
  }

  checkLoggedIn($userLoggedIn: Observable<boolean>, id: string) {
    $userLoggedIn.pipe(take(1)).subscribe(isLoggedIn => {
      if (isLoggedIn) {
        this.result[id] = 'routed to dashboard';
      } else {
        this.result[id] = 'routed to landing page';
      }
    });
  }

结果如下:

{
  "behaviorSubject": "routed to landing page",
  "replaySubject": "routed to dashboard"
}

在这种情况下,你需要一个ReplaySubject!工作代码:https://stackblitz.com/edit/replaysubject-vs-behaviorsubject?file=src%2Fapp%2Fapp.component.ts

0

另一个区别是你可以使用BehaviorSubject的值获取器来获取当前值。在某些情况下,这非常有用,当你只需要当前值时。例如,当用户单击某个东西并且你仅需要该值一次时。在这种情况下,你不需要订阅然后突然取消订阅。唯一需要的是:

BehaviorSubject bSubject = new BehaviorSubject<IBasket>(basket);

getCurrentBasketValue() {
  return this.bSubject.value;
}

0
     // ***********Subject  concept ***********
    let subject = new Subject<string>();


    subject.next("Eureka");
    subject.subscribe((data) => {
      console.log("Subscriber 1 got data >>>>> "+ data);
    });
    subject.subscribe((data) => {
      console.log("Subscriber 2 got data >>>>> "+ data);
    });

       // ********behaviour subject*********
    // Behavior subjects need a first value
let subject1 = new BehaviorSubject<string>("First value");


subject1.asObservable().subscribe((data) => {
  console.log("First subscriber got data behaviour subject>>>>> "+ data);
});
subject1.next("Second value")
  • 主题 - 订阅者只会在订阅之后收到已发布的值。
  • BehaviorSubject - 新的订阅者在订阅时会立即收到最新发布的值或初始值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接