理解 rxjs 中的 subject

烂漫一生 提交于 2020-03-17 12:52:32

某厂面试归来,发现自己落伍了!>>>

已经学习了很久的 angular 了,rxjs 中的操作数也有很多都打过了照面,subject 也已经可以仿着别人的栗子,能写个大概的轮廓了,可是我真的理解了、掌握了 rxjs 中的 subject 吗?

以前有人告诉过我:在生活中学到的所有东西中,真正让人难以忘怀的就是我们教给其他人的东西。所以现在就把它慢慢整理出来,如果没有理解透彻 subject 的你看到了,很开心你会有所收获;

背景:为什么我们需要 subject ?

理解 hot Observable 和 cold observable

hot Observable: 每次被 subscribe 都会产生一个全新的数据序列数据流;(创建类操作符、interval、range);也就是虽然对一个 Observable 做了多次 subscribe, 但是对于每一个 Observer, 其实都有一个独立的数据流在喂着,数据并不是真正来自于同一个源头;也就是不同订阅者的订阅实则是相互独立,互不干扰的;比如下面这个例子:

    const source$ = interval(500).pipe(take(3));
    const observerA = {
          next: value => console.log('A' + value),
          error: error => console.log('A' + error),
          complete: () => console.log('A complete')
        };
    const observerB = {
          next: value => console.log('b' + value),
          error: error => console.log('b' + error),
          complete: () => console.log('b complete')
        };
    console.log(source$.subscribe(observerA));
    setTimeout(() => {
    	console.log(source$.subscribe(observerB));
    }, 1000)

上面 observerA和observerB分别订阅了source,从 log 可以看出两个观察者都接收到了完整的数据序列;也就是这两个 observer 是分开执行,是独立的;

Hot Observable: 在创建类操作符中,fromPromise、fromEvent、fromEventPattern 是hot Observable;不难看出,产生 hot Observable 的操作符数据源在外部,或来自 promise,或来自DOM,或来自Event emitter,真正的数据源和有没有 Observer 没有任何关系; 比如以下这个栗子:

        const source$ = interval(500).pipe(take(3), share());
        const observerA = {
          next: value => console.log('A' + value),
          error: error => console.log('A' + error),
          complete: () => console.log('A complete')
        };
        const observerB = {
          next: value => console.log('b' + value),
          error: error => console.log('b' + error),
          complete: () => console.log('b complete')
        };
        console.log(source$.subscribe(observerA));
        setTimeout(() => {
          console.log(source$.subscribe(observerB));
        }, 1000);

从 log 可以看出,当observerB 订阅时,接收到的元素是接着observerA的订阅往下发的;而不是从新开始;

满足Hot Observable 的例子不少,比如浏览器中鼠标的移动事件、点击时间、浏览器中的滚动事件,来自 websocket 的推送消息,还有 nodejs 支持的EventEmitter 对象消息;

现在有这么一种情况:如果我们想要在定一个栗子的基础上,我们订阅source 不会从头开始接收元素,而是从上次订阅到当前处理的元素开始接受元素,我们该怎么办呢?也就是我们想要把 cold Observable 转换成 hot Observable ,我们该如何操作?

subject 实现机制和使用

获取有人已经想到可以通过一个中介来实现,也就是中介来订阅 source, 让中介把数据转送出去;

    const source$ = interval(500).pipe(take(3));
    const observerA = {
          next: value => console.log('A' + value),
          error: error => console.log('A' + error),
          complete: () => console.log('A complete')
        };
    const observerB = {
          next: value => console.log('b' + value),
          error: error => console.log('b' + error),
          complete: () => console.log('b complete')
        };
    
    const subject = {
    	observers: [],
    	subscribe: () => this.observers.push(observer),
    	next: () => this.observers.foreach(o => o.next(error)),
    	error: () => this.observers.foreach(o => o.error(error)),
    	complet: () => this.observers.foreach(o => o.com[lete())
    }
    
    source$.subscribe(subject);
    
    console.log(subject.subscribe(observerA);));
    setTimeout(() => {
    	console.log(source$.subscribe(observerB));
    }, 1000)
    
    
    // A0
    // A1
    // B1
    // A2
    // B2
    // A complete
    // B complete

从这个代码可以看出,这个中介就是 subject,我们创建了一个 subject 变量,它同时具备 observer 所有的方法(next, error, complete), 还有 observable 的 subscribe ;每当有值送出,就会遍历清单中所有的observer,并再次把值送出,这样一来不管多久之后加进来的 observer, 都会是从当前处理到的元素接着往下走;把上面的代码改成 rxjs 中subject 代码是这样的:

    const source$ = interval(500).pipe(take(3));
    const observerA = {
          next: value => console.log('A' + value),
          error: error => console.log('A' + error),
          complete: () => console.log('A complete')
        };
    const observerB = {
          next: value => console.log('b' + value),
          error: error => console.log('b' + error),
          complete: () => console.log('b complete')
        };
    
    const subject = new Subject();
    source$.subscribe(subject);
    console.log(subject.subscribe(observerA);));
    setTimeout(() => {
    	console.log(source$.subscribe(observerB));
    }, 1000)
    
    
    // A0
    // A1
    // B1
    // A2
    // B2
    // A complete
    // B complete

打印结果是一致的;也就是说 subject 的执行机制其实跟我们猜想的一样;建立一个 subject 先订阅 observable(数据源),再把我们的真正的 observer 加到 subject 中,这样一来就能完成订阅,而每个加到subject 中的 observer 都能整组的接收到相同的元素;总结出来就是两句话:

  • Subject 同时是 Observable,又是 Observer;
  • Subject 会对内部的Observable 清单进行多播;

我们还可以看看 subject 的源码:

    export class Subject<T> extends Observable<T> {
    	observers: Observer<T>[] = [];// 订阅者列表
    	closed = false;
      isStopped = false;
      hasError = false;
    
    	constructor() {
        super();
      }
    
    	next(value?: T) {
    	    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++) {
    	        copy[i].next(value!);
    	      }
    	    }
    	  }
    
      error(err: any) {
        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++) {
          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++) {
          copy[i].complete();
        }
        this.observers.length = 0;
      }
    
      unsubscribe() {
        this.isStopped = true;
        this.closed = true;
        this.observers = null!;
      }
    
    	asObservable(): Observable<T> {
        const observable = new Observable<T>();
        (<any>observable).source = this;
        return observable;
      }
    }

Subject既是Observable又是Observer, 是Observable是因为它继承于Observable 类, 是 Observer是因为它也有 next、error、complete方法,可用作subscribe的参数。 不同于Observable,它可以向多个Observer多路推送数值。普通的Observable并不具备多路推送的能力(每一个Observer都有自己独立的执行环境),而 Subject可以共享一个执行环境。

subject 的使用

我们可以用 subject 的next 方法传送值,所有订阅的 observer 就会接收到,又因为 subject 本身是 observable , 所以这样的使用方式很适合在某些无法直接使用 observable 的情况中;

subject 三种变形:BahaviorSubject、ReplaySuject、AsyncSubject;

  • BehaviorSubject: 很多时候我们会希望 subject 能代表当下的状态,而不是单纯的事件发送,也就是说如果今天有一个新的订阅,我们希望 subject 能立即给出最新的值,而不是没有回应,例如下面:
    const  subject = new Subject();
    
    const observerA = {
        next: value => console.log('A' + value),
        error: error => console.log('A' + error),
        complete: () => console.log('A complete!')
    }
    
    const observerB = {
        next: value => console.log('B' + value),
        error: error => console.log('B' + error),
        complete: () => console.log('B complete!')
    }
    
    subject.subscribe(observerA);
    
    subject.next(1);
    // A1
    subject.next(2);
    // A2
    subject.next(3);
    // A3
    
    setTimeout(() => {
        subject.subscribe(observerB); // 3 s才订阅,b 不会收到任何值
    },3000)

以这个例子来说, observerB 订阅之后,是不会有任何元素送给 observerB 的, 因为在这之后没有执行任何 subject.next(), 但很多时候我们希望 subject 能表达当前的状态,在一订阅时就能收到最新的状态是什么,而不是订阅之后要等到有变动才能接受到新的额状态,以这个例子来说,我们希望 observerB 马上就能收到 3,这是就可以用 BehaviorSubject;

  • BehaviorSubject:它跟Subject 最大的不同是前者是用来呈现当前的值,而不是单纯的发送事件;前者会记住最新一次发送的元素,并把该元素当成目前的值,在使用上前者需要传入一个参数来代表起始的状态;如
    const subject = new Subject(0);// 0为起始值
  • ReplaySubject: 在某些时候我们会希望 Subject 代表事件,但又能在新订阅时重新发送最后的几个元素,这时就用这个;
    const subject = new ReplaySubject(2); // 重复发送最后两个元素
    const observerA = {
        next: value => console.log('A' + value),
        error: error => console.log('A' + error),
        complete: () => console.log('A complete!')
    }
    
    var observerB = {
        next: value => console.log('B' + value),
        error: error => console.log('B' + error),
        complete: () => console.log('B complete!')
    }
    
    subject.subscribe(observerA);
    subject.next(1);
    // A1
    subject.next(2);
    // A2
    subject.next(3);
    // A3
    
    setTimeout(() => {
        subject.subscribe(observerB);
        // B2
        // B3
    },3000)

可能会有人以为 ReplaySubject(1) 是不是就等于 BehaviorSubject,其实是不一样的;BehaviorSubject在建立时就有初始值,代表的是初始状态,但ReplaySubject 只是事件的重放而已;

  • AsyncSubject: 会在subject 结束后发送出最后一个值;如下面这个例子,AsyncSubject 用得比较少;
    const subject = new AsyncSubject();
    const observerA = {
        next: value => console.log('A' + value),
        error: error => console.log('A' + error),
        complete: () => console.log('A complete!')
    }
    
    const observerB = {
        next: value => console.log('B' + value),
        error: error => console.log('B' + error),
        complete: () => console.log('B complete!')
    }
    
    subject.subscribe(observerA);
    subject.next(1);
    subject.next(2);
    subject.next(3);
    subject.complete();
    // "A3"
    // "A complete!"
    
    setTimeout(() => {
        subject.subscribe(observerB);
        // "B3"
        // "B complete!"
    },3000)
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!