In software architecture, publish-subscribe is a messaging paradigm in which a sender of a message (called a publisher) does not directly send the message to a specific recipient (called a subscriber). Instead, they divide published messages into different categories without knowing which subscribers, if any, might exist. Similarly, subscribers can express interest in one or more categories and receive only messages of interest without knowing which publishers (if any) exist. (Source: Publish/subscribe wikipedia)

scenario

Publish-subscribe is common for front-end development. The following scenarios actually use the publish-subscribe model.

  • DOMIn the operationaddEventListener.
  • VueThe concept of an event bus in.
  • Node.jsIn theEventEmitterAnd built-in libraries.

What is publish-subscribe

Publish-subscribe is a one-to-many dependency among objects in which all subscribed objects are notified when an object fires an event.

For example, if you subscribe to a topic on a platform, the system will automatically push information to you when the topic is updated, without you having to manually check it. This example is similar to the publish-subscribe model.

There are a few concepts that need to be clarified

Publisher: Distribute events through the event center

Subscriber: Subscribes to events through the event center

Event center: Is responsible for storing the relationship between events and subscribers.

How to implement a publish-subscribe model

Basic version

Ideas:

  • Our event center we useMapStructure for storage.MapIn the event namekeyAnd thevalueIs to store the corresponding eventsThe subscriber.
  • onMethods are event subscribed methods that can be based ontypeTo find the corresponding set of subscribers to the function we will pass incallbackAdd to the collection.
  • emitMethods are the methods by which events are published, according totypeTo find the corresponding set of subscribers and call the subscribers in turn.
  • offThe method is the unsubscribe method, according totypeTo find the corresponding set of subscribers, essentially matching the corresponding subscribers according to the reference and moving them out of the set.

The code is as follows:

class EventEmmiter {
  subscribes: Map<string.Array<Function> >;constructor() {
    this.subscribes = new Map(a); }/** * Event subscription *@param Type Subscribed event name *@param Callback triggers the callback function */
  on(type: string, callback: Function) {
    const sub = this.subscribes.get(type) | | []; sub.push(callback);this.subscribes.set(type, sub);
  }

  /** * Release event *@param Type Published event name *@param Args publishes additional arguments to the event */
  emit(type: string. args:Array<any>) {
    const sub = this.subscribes.get(type) | | [];const context = this;
    sub.forEach(fn= > {
      fn.call(context, ...args);
    })
  }

  /** * Unsubscribe *@param Type Name of the unsubscribed event *@param Callback Unsubscribes the specific event */
  off(type: string, callback: Function) {
    const sub = this.subscribes.get(type);
    
    if(sub) {
      const newSub = sub.filter(fn= >fn ! == callback);this.subscribes.set(type, newSub); }}}const eventEmmiter = new EventEmmiter();

eventEmmiter.on('eventName'.() = > {
  console.log('First Subscription');
});
eventEmmiter.emit('eventName');

const secondEmmiter = (a: number, b: number) = > {
  console.log('Second subscription, result is${a + b}`);
}
eventEmmiter.on('eventName', secondEmmiter);
eventEmmiter.emit('eventName'.1.3);

eventEmmiter.off('eventName', secondEmmiter);
eventEmmiter.emit('eventName'.1.3);
Copy the code

The output

First subscription first subscription second subscription is 4 first subscriptionCopy the code

As expected, we now have a publish-subscribe version of the simpler version

Support once subscription

All of us who have used publish-subscribe should know that publish-subscribe direct subscription means that the subscriber is notified only once. Now, let’s add this operation.

Our subscriber needs to have the once attribute for judgment, and after each emit, we need to clear the once event.

With this in mind, we need to change the data structure of our subscribers.

// Previous subscribers (essentially functions)
subscribes: Map<string.Array<Function> >;// Current subscribers
interface SubscribeEvent {
  fn: Function;
  once: boolean;
}

subscribes: Map<string.Array<SubscribeEvent>>;
Copy the code

At the same time, we find that the logic of once and on only lies in the difference of parameters, and this part is extracted. So we have the following code

interface SubscribeEvent {
  fn: Function;
  once: boolean;
}

class EventEmmiter {
  subscribes: Map<string.Array<SubscribeEvent>>;

  constructor() {
    this.subscribes = new Map(a); }addEvent(type: string, callback: Function, once: boolean = false) {
    const sub = this.subscribes.get(type) | | []; sub.push({fn: callback, once });
    this.subscribes.set(type, sub);
  }

  on(type: string, callback: Function) {
    this.addEvent(type, callback);
  }

  emit(type: string. args:Array<any>) {
    const sub = this.subscribes.get(type) | | [];const context = this;
    
    sub.forEach(({ fn }) = >{ fn.call(context, ... args); });const newSub = sub.filter(item= >! item.once);this.subscribes.set(type, newSub);
  }

  off(type: string, callback: Function) {
    const sub = this.subscribes.get(type);
    
    if(sub) {
      const newSub = sub.filter(({ fn }) = >fn ! == callback);this.subscribes.set(type, newSub); }}once(type: string, callback: Function) {
    this.addEvent(type, callback, true); }}const eventEmmiter = new EventEmmiter();
eventEmmiter.on('eventName'.() = > {
  console.log('First Subscription');
});

eventEmmiter.once('eventName'.() = > {
  console.log('once test')}); eventEmmiter.emit('eventName');

eventEmmiter.emit('eventName');
Copy the code

Output results:

First subscription Once test First subscriptionCopy the code

Above, our once subscription is also supported.

Release the cache

Based on the above code, we consider a scenario where if no subscriber has subscribed to the event, but our event center publishes the event, then the publication is invalid. This may be acceptable, but in certain scenarios we need to cache published events and call them when a subscriber subscribes.

We use _cacheQueue to cache published events. Since we can publish events with parameters, we need a collection/array to hold those parameters.

type CacheArgs = Array<any>;
_cacheQueue: Map<string.Array<CacheArgs>>;
Copy the code

The final code is as follows

interface SubscribeEvent {
  fn: Function;
  once: boolean;
}

type CacheArgs = Array<any>;

class EventEmmiter {
  subscribes: Map<string.Array<SubscribeEvent>>;
  _cacheQueue: Map<string.Array<CacheArgs>>;

  constructor() {
    this.subscribes = new Map(a);this._cacheQueue = new Map(a); }addEvent(type: string, callback: Function, once: boolean = false) {
    const cache = this._cacheQueue.get(type) | | [];if(cache.length ! = =0) {
      cache.forEach(args= >{ callback(... args); })this._cacheQueue.delete(type);
    }
    const sub = this.subscribes.get(type) | | []; sub.push({fn: callback, once });
    this.subscribes.set(type, sub);
  }

  on(type: string, callback: Function) {
    this.addEvent(type, callback);
  }

  emit(type: string. args:Array<any>) {
    const sub = this.subscribes.get(type) | | [];if(sub.length === 0) {
      const cache = this._cacheQueue.get(type) | | []; cache.push(args)this._cacheQueue.set(type, cache);
    } else {
      const context = this;
      
      sub.forEach(({ fn }) = >{ fn.call(context, ... args); });const newSub = sub.filter(item= >! item.once);this.subscribes.set(type, newSub); }}off(type: string, callback: Function) {
    const sub = this.subscribes.get(type);
    
    if(sub) {
      const newSub = sub.filter(({ fn }) = >fn ! == callback);this.subscribes.set(type, newSub); }}once(type: string, callback: Function) {
    this.addEvent(type, callback, true); }}const eventEmmiter = new EventEmmiter();

eventEmmiter.emit('test_cache'.1.2);

eventEmmiter.emit('test_cache'.1.3);

eventEmmiter.on('test_cache'.(a: number, b: number) = > {
  console.log("Subscribed after the event was published, calculated as",a + b);
});
eventEmmiter.on('test_cache'.(a: number, b: number) = > {
  console.log("There is no cache for publish events. No trigger.",a + b);
});
Copy the code

Results:

The calculated value is 3 for those subscribed after the event is published, and the calculated value is 4Copy the code

As expected, a simple publish-subscribe model is in place.

The difference between publish-subscribe and observer patterns

I think the difference may lie in how I understand the publish-subscribe versus observer model.

The publish-subscribe model is a generalized observer pattern, a one-to-many dependency among objects, in which all subscribed objects are notified when an object fires an event.

The publish-subscribe pattern is a commonly used implementation of the observer pattern and is superior to the typical observer pattern in terms of decoupling and reuse.

  • In the observer mode, the observer needs to subscribe directly to the target event; After the target issues an event that changes the content, the event is received directly and the response is made.
  • In the publish-subscribe model, publishers and subscribers have one more publishing channel; Receiving events from publishers on the one hand and publishing them to subscribers on the other; Subscribers need to subscribe to events from the event channel.

conclusion

The publish-subscribe scenario and simple implementation were described above. For those who want to take a closer look at EventEmitter from Node.js, learn how to use it in your own business scenarios.

advantages

  • Modules are decoupled, not strongly associated with specific other modules, and only need to subscribe to related events.
  • In asynchronous programming, code can be loosely coupled.

disadvantages

  • Loose coupling weakens relationships between objects and can be difficult for programs to trace during debugging.

Finally, thank you for reading this article, it is not very in-depth, there may be some small mistakes or clerical errors, please forgive me, you can comment in the comments section below, thank you very much. In addition, if this article is a little help or inspiration to you, I hope you can click a like ha, support is the motivation of creation ~