Thursday, October 31, 2019

EventEmitter, Promise, Observable, Subject, BehaviorSubject difference

In Angular project, notification information can be passed from sender's web component to receiver's web component through eventEmitter, Observable and Subject. Depends on the requirement, different type should be selected.

Assuming in sender's web component, when a button is clicked, a new stock price should be updated and received by receiver's web component. The logic of managing the notification message is implemented in a separate service component

1. using EventEmitter

The messaging service should be registered in app.component.ts as a service provider
import { EventEmitter } from '@angular/core';
export class MessagingService {
    stockEventEmitter = new EventEmitter<number>();
}
sender.ts
import { MessagingService } from '../messaging/messaging.service';
export class Cmp1Component implements OnInit {
  stockPrice = 100;
constructor(public activatedRoute: ActivatedRoute, public router: Router, private messaging: MessagingService  ) 
{ }

onUpdateStockPrice(priceChange: number) {
    this.stockPrice = this.stockPrice + priceChange;
    this.messaging.stockEventEmitter.emit(this.stockPrice);
  }
}


Receiver.ts
import { MessagingService } from '../messaging/messaging.service';
export class AppComponent implements OnInit, OnDestroy {
  stockPrice: number;
  constructor(private router: Router,
              public activatedRoute: ActivatedRoute,
              private messaging: MessagingService) {
  }
  ngOnInit() {
    this.messaging.stockEventEmitter.subscribe( price => {
      this.stockPrice = price;
    });
  }
For eventEmitter, the event source is shared by all subscribers, all subscribers are passive listeners, and the event source decides when to send the updated event data to the listeners.
In addition, eventEmitter can only emits the same data type defined in emitter constructor, and does not provide ways to allow sender tells receiver if error happens, or if this messaging operation is ended.  This issue can be solved using Angular Subscribe.
2. Promise

Similar to event, promise pushes data to listener when it is ready, so listener does not need pull the data by itself. However, a promise instance cannot be shared by multiple listeners, each promise object can only server one listener through the registered callback method, it works like a regular asynchronous function call. 
Besides returning the normal successful response, promise can also return error result to receiver.

    const p = new Promise((resolve, reject) => {
      console.log('Promise operation starts');
    
      const r = Math.random();
      if (r > 0.5) {
          console.log('build world successful');
          resolve(r);
      } else {
          console.log('build world failed');
          reject('world is falling');
     }
  } );

 p.then(r => {
        console.log('home build success with ' + r + ' rooms');
 })
 .catch((e) => {
        console.log(e);
 });
The promise constructor method starts when creating the promise object, even if there is no next method registered to get the result. 
3. Observable
Observable is similar to promise, but the observer can get multiple result set from observable. Actually all promise object can be converted to observable. The observable provides next, error and complete method for sender and receiver to handle the success, error and complete status. 
Although multiple observers can subscribe to the same observable instance, each observer will create its own observable instance by calling the obserable costructor, so unlike event emitter, there is not a shared data source to serve multiple subscribers.
Unlike promise, an observable will only start its life cycle when subscriber subscribes to it. At that moment, observable will execute its constructor method to start emitting data. Its life ends when subscriber calls unsubscribe method, or when observable emits error or complete data.
When used as a messaging service, both the observable object for pushing data out, and the subscriber for receiving data need to be exposed to public. It would be better to if a single object is exposed, which can be used by both sending and receiving the sender and receivers to broadcast and receive messages. This can be done by Angular Subject object.
messaging.service.ts
import { EventEmitter } from '@angular/core';
import { Observable, Subscriber } from 'rxjs';
export class MessagingService {
    stockObservable: Observable<number>; // for receiver to subscribe the notification
    stockSubscriber: Subscriber<number>; // for sender to send notification
    constructor() {
        this.stockObservable = new Observable( (subscriber: Subscriber<number>) => {
            this.stockSubscriber = subscriber;
        });
    }
}

sender.ts 
import { MessagingService } from '../messaging/messaging.service';
import { Subscriber } from 'rxjs';
export class Cmp1Component implements OnInit {
  constructor(public activatedRoute: ActivatedRoute, public router: Router, private messaging: MessagingService  ) { }

  onUpdateStockPriceByObservable(priceChange: number) {
    this.stockPrice = this.stockPrice + priceChange;
    this.messaging.stockSubscriber.next(this.stockPrice);
  }
  onErrorStockPriceByObservable() {
    this.messaging.stockSubscriber.error('error happened in stock price update');
  }
  onCompleteStockPriceByObservable() {
    this.messaging.stockSubscriber.complete();
  }
}
receiver.ts
import { intervalSubscription } from 'rxjs';
import { MessagingService } from '../messaging/messaging.service';

export class AppComponent implements OnInit, OnDestroy {
  private stockObservableSub: Subscription;
  constructor(private router: Router,
              public activatedRoute: ActivatedRoute,
              private messaging: MessagingService) {
  }
  ngOnInit() {
    this.stockObservableSub = this.messaging.stockObservable.subscribe( price => {
      this.stockPrice = price;
    }, error => {
      this.stockPrice = -1;
      this.stockPriceError = error;
    }, () => {
      this.stockPrice = -1;
      this.stockPriceError = 'observable completed!';
    }
    );
  }
  ngOnDestroy(): void {
    this.sub.unsubscribe();
    this.eventSub.unsubscribe();
    this.stockObservableSub.unsubscribe();
  }
}

3. Subject
Subject is a special type of observable, and can broadcast multiple data set to multiple subscribers. This is similar to event emitter. 
Subject is both functions to allow multiple subscribers to subscribe to it, and function to emit new data.
One feature missed in regular Subject object is it is not sticky. So after a listener registers a Subject, it will not automatically get the last value sent by the Subject, until the next time when Subject sends new message to listeners. This may not work in all scenarios, for example, as a stock price listener, when a receiver registers to get the price for a stock, it should immediately receive the last price broadcast by sender, without having to wait to get any value until the next stock price update. In this case, BehaviorSubject can be used to receive the initial default value sent by sender.

messging.service.ts
import { EventEmitter } from '@angular/core';
import { Observable, Subscriber, Subject } from 'rxjs';
export class MessagingService {
    stockSubject = new Subject<number>();
}
sender.ts
import { Component, OnInit } from '@angular/core';
import { MessagingService } from '../messaging/messaging.service';
import { Subscriber } from 'rxjs';
export class Cmp1Component implements OnInit {
  constructor(public activatedRoute: ActivatedRoute, public router: Router, private messaging: MessagingService  ) { }
  onUpdateStockPriceBySubject(priceChange: number) {
    this.stockPrice = this.stockPrice + priceChange;
    this.messaging.stockSubject.next(this.stockPrice);
  }
  onErrorStockPriceBySubject() {
    this.messaging.stockSubject.error('error happened in stock price subject');
  }
  onCompleteStockPriceBySubject() {
    this.messaging.stockSubject.complete();
  }
}
receiver.ts
import { Component, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core';
import { interval, Subscription } from 'rxjs';
import { MessagingService } from '../messaging/messaging.service';
export class AppComponent implements OnInit, OnDestroy {
  private stockSubjectSub: Subscription;
  stockPrice: number;
  stockPriceError: string;
  constructor(private router: Router,
    public activatedRoute: ActivatedRoute,
    private messaging: MessagingService) {
  }
  ngOnInit() {
    this.stockSubjectSub = this.messaging.stockSubject.subscribe(price => {
      this.stockPrice = price;
    }, error => {
      this.stockPrice = -1;
      this.stockPriceError = error;
    }, () => {
      this.stockPrice = -1;
      this.stockPriceError = 'subject completed!';
    }
    );
  }
}
4. BehaviorSubject, ReplaySubject
BehaviorSubject is a special Subject, it allows to set an initial value in constructor, so there is always a value to be sent to subscriber when subscribing to a behaviorSubject. If next method is called on subscriber later, then the new value will be read by the observer's next method.
messaging.ts
import { EventEmitter } from '@angular/core';
import { Observable, Subscriber, Subject, BehaviorSubject } from 'rxjs';
export class MessagingService {
    stockBehaviorSubject = new BehaviorSubject<number>(1000);
}

ReplaySubject enables subscriber to specify how many old value, or how long of time should be cached and replayed to the new subscriber.

AsyncSubject is a subject where only the last value of the Observable is sent to its observers, and only when the execution completes.

No comments:

Post a Comment