关于RxJS Subject的学习笔记


Posted in Javascript onDecember 05, 2018

Observer Pattern

观察者模式定义

观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

我们可以使用日常生活中,期刊订阅的例子来形象地解释一下上面的概念。期刊订阅包含两个主要的角色:期刊出版方和订阅者,他们之间的关系如下:

  • 期刊出版方 - 负责期刊的出版和发行工作
  • 订阅者 - 只需执行订阅操作,新版的期刊发布后,就会主动收到通知,如果取消订阅,以后就不会再收到通知

在观察者模式中也有两个主要角色:Subject (主题) 和 Observer (观察者) 。它们分别对应例子中的期刊出版方和订阅者。接下来我们来看张图,从而加深对上面概念的理解。

关于RxJS Subject的学习笔记

观察者模式结构

关于RxJS Subject的学习笔记

观察者模式实战

Subject 类定义

class Subject {
  
  constructor() {
    this.observerCollection = [];
  }
  
  addObserver(observer) { // 添加观察者
    this.observerCollection.push(observer);
  }
  
  deleteObserver(observer) { // 移除观察者
    let index = this.observerCollection.indexOf(observer);
    if(index >= 0) this.observerCollection.splice(index, 1);
  }
  
  notifyObservers() { // 通知观察者
    this.observerCollection.forEach((observer)=>observer.notify());
  }
}

Observer 类定义

class Observer {
  constructor(name) {
    this.name = name;
  }
  
  notify() {
    console.log(`${this.name} has been notified.`);
  }
}

使用示例

let subject = new Subject(); // 创建主题对象

let observer1 = new Observer('semlinker'); // 创建观察者A - 'semlinker'
let observer2 = new Observer('lolo'); // 创建观察者B - 'lolo'

subject.addObserver(observer1); // 注册观察者A
subject.addObserver(observer2); // 注册观察者B
 
subject.notifyObservers(); // 通知观察者

subject.deleteObserver(observer1); // 移除观察者A

subject.notifyObservers(); // 验证是否成功移除

以上代码成功运行后控制台的输出结果:

semlinker has been notified.
lolo has been notified.
lolo has been notified.

Observable subscribe

在介绍RxJS - Subject 之前,我们先来看个示例:

const interval$ = Rx.Observable.interval(1000).take(3);

interval$.subscribe({
 next: value => console.log('Observer A get value: ' + value);
});

setTimeout(() => {
 interval$.subscribe({
   next: value => console.log('Observer B get value: ' + value);
 });
}, 1000);

以上代码运行后,控制台的输出结果:

Observer A get value: 0
Observer A get value: 1
Observer B get value: 0
Observer A get value: 2
Observer B get value: 1
Observer B get value: 2

通过以上示例,我们可以得出以下结论:

  • Observable 对象可以被重复订阅
  • Observable 对象每次被订阅后,都会重新执行

上面的示例,我们可以简单地认为两次调用普通的函数,具体参考以下代码:

function interval() {
 setInterval(() => console.log('..'), 1000);
}

interval();

setTimeout(() => {
 interval();
}, 1000);

Observable 对象的默认行为,适用于大部分场景。但有些时候,我们会希望在第二次订阅的时候,不会从头开始接收 Observable 发出的值,而是从第一次订阅当前正在处理的值开始发送,我们把这种处理方式成为组播 (multicast),那我们要怎么实现呢 ?回想一下我们刚才介绍过观察者模式,你脑海中是不是已经想到方案了。没错,我们可以通过自定义 Subject 来实现上述功能。

自定义 Subject

Subject 类定义

class Subject {  
  constructor() {
    this.observers = [];
  }
  
  addObserver(observer) { 
    this.observers.push(observer);
  }
  
  next(value) { 
    this.observers.forEach(o => o.next(value));  
  }
  
  error(error){ 
    this.observers.forEach(o => o.error(error));
  }
  
  complete() {
    this.observers.forEach(o => o.complete());
  }
}

使用示例

const interval$ = Rx.Observable.interval(1000).take(3);
let subject = new Subject();

let observerA = {
  next: value => console.log('Observer A get value: ' + value),
  error: error => console.log('Observer A error: ' + error),
  complete: () => console.log('Observer A complete!')
};

var observerB = {
  next: value => console.log('Observer B get value: ' + value),
  error: error => console.log('Observer B error: ' + error),
  complete: () => console.log('Observer B complete!')
};

subject.addObserver(observerA); // 添加观察者A
interval$.subscribe(subject); // 订阅interval$对象
setTimeout(() => {
  subject.addObserver(observerB); // 添加观察者B
}, 1000);

以上代码运行后,控制台的输出结果:

Observer A get value: 0
Observer A get value: 1
Observer B get value: 1
Observer A get value: 2
Observer B get value: 2
Observer A complete!
Observer B complete!

通过自定义 Subject,我们实现了前面提到的功能。接下来我们进入正题 - RxJS Subject。

RxJS Subject

首先我们通过 RxJS Subject 来重写一下上面的示例:

const interval$ = Rx.Observable.interval(1000).take(3);
let subject = new Rx.Subject();

let observerA = {
  next: value => console.log('Observer A get value: ' + value),
  error: error => console.log('Observer A error: ' + error),
  complete: () => console.log('Observer A complete!')
};

var observerB = {
  next: value => console.log('Observer B get value: ' + value),
  error: error => console.log('Observer B error: ' + error),
  complete: () => console.log('Observer B complete!')
};

subject.subscribe(observerA); // 添加观察者A
interval$.subscribe(subject); // 订阅interval$对象
setTimeout(() => {
  subject.subscribe(observerB); // 添加观察者B
}, 1000);

RxJS Subject 源码片段

/**
 * Suject继承于Observable 
 */
export class Subject extends Observable {
  constructor() {
    super();
    this.observers = []; // 观察者列表
    this.closed = false;
    this.isStopped = false;
    this.hasError = false;
    this.thrownError = null;
  }
 
  next(value) {
    if (this.closed) {
      throw new ObjectUnsubscribedError();
    }
    if (!this.isStopped) {
      const { observers } = this;
      const len = observers.length;
      const copy = observers.slice();
      for (let i = 0; i < len; i++) { // 循环调用观察者next方法,通知观察者
        copy[i].next(value);
      }
    }
  }
 
  error(err) {
    if (this.closed) {
      throw new ObjectUnsubscribedError();
    }
    this.hasError = true;
    this.thrownError = err;
    this.isStopped = true;
    const { observers } = this;
    const len = observers.length;
    const copy = observers.slice();
    for (let i = 0; i < len; i++) { // 循环调用观察者error方法
      copy[i].error(err);
    }
    this.observers.length = 0;
  }
 
  complete() {
    if (this.closed) {
      throw new ObjectUnsubscribedError();
    }
    this.isStopped = true;
    const { observers } = this;
    const len = observers.length;
    const copy = observers.slice();
    for (let i = 0; i < len; i++) { // 循环调用观察者complete方法
      copy[i].complete();
    }
    this.observers.length = 0; // 清空内部观察者列表
  }
}

通过 RxJS Subject 示例和源码片段,对于 Subject 我们可以得出以下结论:

  • Subject 既是 Observable 对象,又是 Observer 对象
  • 当有新消息时,Subject 会对内部的 observers 列表进行组播 (multicast)

Angular 2 RxJS Subject 应用

在 Angular 2 中,我们可以利用 RxJS Subject 来实现组件通信,具体示例如下:

message.service.ts

import { Injectable } from '@angular/core';
import {Observable} from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class MessageService {
  private subject = new Subject<any>();

  sendMessage(message: string) {
    this.subject.next({ text: message });
  }

  clearMessage() {
    this.subject.next();
  }

  getMessage(): Observable<any> {
    return this.subject.asObservable();
  }
}

home.component.ts

import { Component } from '@angular/core';

import { MessageService } from '../_services/index';

@Component({
  moduleId: module.id,
  templateUrl: 'home.component.html'
})

export class HomeComponent {
  constructor(private messageService: MessageService) {}
  
  sendMessage(): void { // 发送消息
    this.messageService.sendMessage('Message from Home Component to App Component!');
  }

  clearMessage(): void { // 清除消息
    this.messageService.clearMessage();
  }
}

app.component.ts

import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

import { MessageService } from './_services/index';

@Component({
  moduleId: module.id,
  selector: 'app',
  templateUrl: 'app.component.html'
})

export class AppComponent implements OnDestroy {
  message: any;
  subscription: Subscription;

  constructor(private messageService: MessageService) {
    this.subscription = this.messageService.getMessage()
       .subscribe(message => { this.message = message; });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

以上示例实现的功能是组件之间消息通信,即 HomeComponent 子组件,向 AppComponent 父组件发送消息。代码运行后,浏览器的显示结果如下:

关于RxJS Subject的学习笔记

Subject 存在的问题

因为 Subject 在订阅时,是把 observer 存放到观察者列表中,并在接收到新值的时候,遍历观察者列表并调用观察者上的 next 方法,具体如下:

next(value) {
    if (this.closed) {
      throw new ObjectUnsubscribedError();
    }
    if (!this.isStopped) {
      const { observers } = this;
      const len = observers.length;
      const copy = observers.slice();
      for (let i = 0; i < len; i++) { // 循环调用观察者next方法,通知观察者
        copy[i].next(value);
      }
    }
}

这样会有一个大问题,如果某个 observer 在执行时出现异常,却没进行异常处理,就会影响到其它的订阅者,具体示例如下:

const source = Rx.Observable.interval(1000);
const subject = new Rx.Subject();

const example = subject.map(x => {
  if (x === 1) {
    throw new Error('oops');
  }
  return x;
});
subject.subscribe(x => console.log('A', x));
example.subscribe(x => console.log('B', x));
subject.subscribe(x => console.log('C', x));

source.subscribe(subject);

以上代码运行后,控制台的输出结果:

A 0
B 0
C 0
A 1
Rx.min.js:74 Uncaught Error: oops

JSBin - Subject Problem Demo

在代码运行前,大家会认为观察者B 会在接收到 1 值时抛出异常,观察者 A 和 C 仍会正常运行。但实际上,在当前的 RxJS 版本中若观察者 B 报错,观察者 A 和 C 也会停止运行。那么应该如何解决这个问题呢?目前最简单的方式就是为所有的观察者添加异常处理,更新后的代码如下:

const source = Rx.Observable.interval(1000);
const subject = new Rx.Subject();

const example = subject.map(x => {
  if (x === 1) {
    throw new Error('oops');
  }
  return x;
});

subject.subscribe(
  x => console.log('A', x),
  error => console.log('A Error:' + error)
);
  
example.subscribe(
  x => console.log('B', x),
  error => console.log('B Error:' + error)
);

subject.subscribe(
  x => console.log('C', x),
  error => console.log('C Error:' + error)
);

source.subscribe(subject);

JSBin - RxJS Subject Problem Solved Demo

RxJS Subject & Observable

Subject 其实是观察者模式的实现,所以当观察者订阅 Subject 对象时,Subject 对象会把订阅者添加到观察者列表中,每当有 subject 对象接收到新值时,它就会遍历观察者列表,依次调用观察者内部的 next() 方法,把值一一送出。

Subject 之所以具有 Observable 中的所有方法,是因为 Subject 类继承了 Observable 类,在 Subject 类中有五个重要的方法:

  • next - 每当 Subject 对象接收到新值的时候,next 方法会被调用
  • error - 运行中出现异常,error 方法会被调用
  • complete - Subject 订阅的 Observable 对象结束后,complete 方法会被调用
  • subscribe - 添加观察者
  • unsubscribe - 取消订阅 (设置终止标识符、清空观察者列表)

BehaviorSubject

BehaviorSubject 定义

BehaviorSubject 源码片段

export class BehaviorSubject extends Subject {
  constructor(_value) { // 设置初始值
    super();
    this._value = _value;
  }
  get value() { // 获取当前值
    return this.getValue();
  }
  _subscribe(subscriber) {
    const subscription = super._subscribe(subscriber);
    if (subscription && !subscription.closed) {
      subscriber.next(this._value); // 为新的订阅者发送当前最新的值
    }
    return subscription;
  }
  getValue() {
    if (this.hasError) {
      throw this.thrownError;
    }
    else if (this.closed) {
      throw new ObjectUnsubscribedError();
    }
    else {
      return this._value;
    }
  }
  next(value) { // 调用父类Subject的next方法,同时更新当前值
    super.next(this._value = value);
  }
}

BehaviorSubject 应用

有些时候我们会希望 Subject 能保存当前的最新状态,而不是单纯的进行事件发送,也就是说每当新增一个观察者的时候,我们希望 Subject 能够立即发出当前最新的值,而不是没有任何响应。具体我们先看一下示例:

var subject = new Rx.Subject();

var observerA = {
  next: value => console.log('Observer A get value: ' + value),
  error: error => console.log('Observer A error: ' + error),
  complete: () => console.log('Observer A complete!')
};

var observerB = {
  next: value => console.log('Observer B get value: ' + value),
  error: error => console.log('Observer B error: ' + error),
  complete: () => console.log('Observer B complete!')
};

subject.subscribe(observerA);

subject.next(1);
subject.next(2);
subject.next(3);

setTimeout(() => {
 subject.subscribe(observerB); // 1秒后订阅
}, 1000);

以上代码运行后,控制台的输出结果:

Observer A get value: 1
Observer A get value: 2
Observer A get value: 3

通过输出结果,我们发现在 observerB 订阅 Subject 对象后,它再也没有收到任何值了。因为 Subject 对象没有再调用 next() 方法。但很多时候我们会希望 Subject 对象能够保存当前的状态,当新增订阅者的时候,自动把当前最新的值发送给订阅者。要实现这个功能,我们就需要使用 BehaviorSubject。

BehaviorSubject 跟 Subject 最大的不同就是 BehaviorSubject 是用来保存当前最新的值,而不是单纯的发送事件。BehaviorSubject 会记住最近一次发送的值,并把该值作为当前值保存在内部的属性中。接下来我们来使用 BehaviorSubject 重新一下上面的示例:

var subject = new Rx.BehaviorSubject(0); // 设定初始值

var observerA = {
  next: value => console.log('Observer A get value: ' + value),
  error: error => console.log('Observer A error: ' + error),
  complete: () => console.log('Observer A complete!')
};

var observerB = {
  next: value => console.log('Observer B get value: ' + value),
  error: error => console.log('Observer B error: ' + error),
  complete: () => console.log('Observer B complete!')
};

subject.subscribe(observerA);

subject.next(1);
subject.next(2);
subject.next(3);

setTimeout(() => {
 subject.subscribe(observerB); // 1秒后订阅
}, 1000);

以上代码运行后,控制台的输出结果:

Observer A get value: 0
Observer A get value: 1
Observer A get value: 2
Observer A get value: 3
Observer B get value: 3

JSBin - BehaviorSubject

ReplaySubject

ReplaySubject 定义

ReplaySubject 源码片段

export class ReplaySubject extends Subject {
  constructor(bufferSize = Number.POSITIVE_INFINITY, 
        windowTime = Number.POSITIVE_INFINITY, 
        scheduler) {
    super();
    this.scheduler = scheduler;
    this._events = []; // ReplayEvent对象列表
    this._bufferSize = bufferSize < 1 ? 1 : bufferSize; // 设置缓冲区大小
    this._windowTime = windowTime < 1 ? 1 : windowTime;
  }
 
  next(value) {
    const now = this._getNow();
    this._events.push(new ReplayEvent(now, value));
    this._trimBufferThenGetEvents();
    super.next(value);
  }
 
 _subscribe(subscriber) {
    const _events = this._trimBufferThenGetEvents(); // 过滤ReplayEvent对象列表
    let subscription;
    if (this.closed) {
      throw new ObjectUnsubscribedError();
    }
    ...
    else {
      this.observers.push(subscriber);
      subscription = new SubjectSubscription(this, subscriber);
    }
     ...
    const len = _events.length;
    // 重新发送设定的最后bufferSize个值
    for (let i = 0; i < len && !subscriber.closed; i++) {
      subscriber.next(_events[i].value);
    }
    ...
    return subscription;
  }
}

class ReplayEvent {
  constructor(time, value) {
    this.time = time;
    this.value = value;
  }
}

ReplaySubject 应用

有些时候我们希望在 Subject 新增订阅者后,能向新增的订阅者重新发送最后几个值,这时我们就可以使用 ReplaySubject ,具体示例如下:

var subject = new Rx.ReplaySubject(2); // 重新发送最后2个值

var observerA = {
  next: value => console.log('Observer A get value: ' + value),
  error: error => console.log('Observer A error: ' + error),
  complete: () => console.log('Observer A complete!')
};

var observerB = {
  next: value => console.log('Observer B get value: ' + value),
  error: error => console.log('Observer B error: ' + error),
  complete: () => console.log('Observer B complete!')
};

subject.subscribe(observerA);

subject.next(1);
subject.next(2);
subject.next(3);

setTimeout(() => {
 subject.subscribe(observerB); // 1秒后订阅
}, 1000);

以上代码运行后,控制台的输出结果:

Observer A get value: 1
Observer A get value: 2
Observer A get value: 3
Observer B get value: 2
Observer B get value: 3

可能会有人认为 ReplaySubject(1) 是不是等同于 BehaviorSubject,其实它们是不一样的。在创建BehaviorSubject 对象时,是设置初始值,它用于表示 Subject 对象当前的状态,而 ReplaySubject 只是事件的重放。

JSBin - ReplaySubject

AsyncSubject

AsyncSubject 定义

AsyncSubject 源码片段

export class AsyncSubject extends Subject {
  constructor() {
    super(...arguments);
    this.value = null;
    this.hasNext = false;
    this.hasCompleted = false; // 标识是否已完成
  }
  _subscribe(subscriber) {
    if (this.hasError) {
      subscriber.error(this.thrownError);
      return Subscription.EMPTY;
    }
    else if (this.hasCompleted && this.hasNext) { // 等到完成后,才发出最后的值
      subscriber.next(this.value);
      subscriber.complete();
      return Subscription.EMPTY;
    }
    return super._subscribe(subscriber);
  }
  next(value) {
    if (!this.hasCompleted) { // 若未完成,保存当前的值
      this.value = value;
      this.hasNext = true;
    }
  }
}

AsyncSubject 应用

AsyncSubject 类似于 last 操作符,它会在 Subject 结束后发出最后一个值,具体示例如下:

var subject = new Rx.AsyncSubject();

 var observerA = {
  next: value => console.log('Observer A get value: ' + value),
  error: error => console.log('Observer A error: ' + error),
  complete: () => console.log('Observer A complete!')
 };

 var observerB = {
  next: value => console.log('Observer B get value: ' + value),
  error: error => console.log('Observer B error: ' + error),
  complete: () => console.log('Observer B complete!')
 };

 subject.subscribe(observerA);

 subject.next(1);
 subject.next(2);
 subject.next(3);

 subject.complete();

 setTimeout(() => {
  subject.subscribe(observerB); // 1秒后订阅
 }, 1000);

以上代码运行后,控制台的输出结果:

Observer A get value: 3
Observer A complete!
Observer B get value: 3
Observer B complete!

JSBin - AsyncSubject

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
基于Jquery的文字滚动跑马灯插件(一个页面多个滚动区)
Jul 26 Javascript
jQeury淡入淡出需要注意的问题
Sep 08 Javascript
查看大图功能代码jquery版
Nov 05 Javascript
AngularJS中transclude用法详解
Nov 03 Javascript
利用jquery禁止外层滚动条的滚动
Jan 05 Javascript
JS实现图片高斯模糊切换效果的焦点图实例
Jan 21 Javascript
boostrapTable的refresh和refreshOptions区别浅析
Jan 22 Javascript
vue移动端路由切换实例分析
May 14 Javascript
jQuery实现表单动态添加数据并提交的方法
Jul 19 jQuery
js代码规范之Eslint安装与配置详解
Sep 08 Javascript
vue3.0 CLI - 2.1 -  component 组件入门教程
Sep 14 Javascript
angular4自定义表单控件[(ngModel)]的实现
Nov 23 Javascript
node实现生成带参数的小程序二维码并保存到本地功能示例
Dec 05 #Javascript
详解如何在Angular优雅编写HTTP请求
Dec 05 #Javascript
JS+HTML5 canvas绘制验证码示例
Dec 05 #Javascript
详解关于Angular4 ng-zorro使用过程中遇到的问题
Dec 05 #Javascript
JS实现简单的点赞与踩功能示例
Dec 05 #Javascript
node.js实现为PDF添加水印的示例代码
Dec 05 #Javascript
vue组件之间通信实例总结(点赞功能)
Dec 05 #Javascript
You might like
php中如何使对象可以像数组一样进行foreach循环
2013/08/09 PHP
php实现的一段简单概率相关代码
2016/05/30 PHP
php接口隔离原则实例分析
2019/11/11 PHP
PHP Beanstalkd消息队列的安装与使用方法实例详解
2020/02/21 PHP
原生JS实现表单checkbook获取已选择的值
2013/07/21 Javascript
ExtJS4中的requires使用方法示例介绍
2013/12/03 Javascript
js修改原型的属性使用介绍
2014/01/26 Javascript
浅析javascript的间隔调用和延时调用
2014/11/12 Javascript
IE下支持文本框和密码框placeholder效果的JQuery插件分享
2015/01/31 Javascript
JS代码实现table数据分页效果
2016/05/26 Javascript
JavaScript中windows.open()、windows.close()方法详解
2016/07/28 Javascript
JavaScript之浏览器对象_动力节点Java学院整理
2017/07/03 Javascript
Angular指令之restict匹配模式的详解
2017/07/27 Javascript
jQuery实现手势解锁密码特效
2017/08/14 jQuery
JS判断两个数组或对象是否相同的方法示例
2019/02/28 Javascript
微信小程序 搜索框组件代码实例
2019/09/06 Javascript
Vue使用Ref跨层级获取组件的步骤
2021/01/25 Vue.js
[04:22]DOTA2上海特级锦标赛主赛事第四日TOP10
2016/03/06 DOTA
[47:43]完美世界DOTA2联赛PWL S3 Magama vs GXR 第二场 12.19
2020/12/24 DOTA
python分析网页上所有超链接的方法
2015/05/08 Python
Python中数组,列表:冒号的灵活用法介绍(np数组,列表倒序)
2018/04/18 Python
python 读取txt,json和hdf5文件的实例
2018/06/05 Python
Python multiprocessing多进程原理与应用示例
2019/02/28 Python
python实现关闭第三方窗口的方法
2019/06/28 Python
python 解压、复制、删除 文件的实例代码
2020/02/26 Python
Python处理mysql特殊字符的问题
2020/03/02 Python
Django用数据库表反向生成models类知识点详解
2020/03/25 Python
python开根号实例讲解
2020/08/30 Python
保安部任务及岗位职责
2014/02/25 职场文书
第一批党的群众路线教育实践活动总结报告
2014/07/03 职场文书
幼儿园中班区域活动总结
2014/07/09 职场文书
国际贸易系求职信
2014/08/09 职场文书
Python初学者必备的文件读写指南
2021/06/23 Python
Javascript中async与await的捕捉错误详解
2022/03/03 Javascript
openGauss数据库JDBC环境连接配置的详细过程(Eclipse)
2022/06/01 Java/Android
MySQL运行报错:“Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre”解决方法
2022/06/14 MySQL