This is the 7th day of my participation in the August More Text Challenge

Publisher-subscriber model

While you may not be familiar with the publisher-subscriber model, you’ve certainly used it. Because the publisher-subscriber model is everywhere on the front end.

Why say so, because the EventTarget. AddEventListener () is a post – subscriber model. Just to keep you in suspense, you’ll understand by the end of this article.

define

The publish-subscriber pattern is really a one-to-many dependency between objects (using message queues). When an object’s state changes, all dependent objects are notified of the state change.

Subscriber registers the Event they want to Subscribe to the Event Channel. When Publisher publishes the Event to the Event Channel, that is, when the Event is triggered, The processing code registered with the dispatch center by the Fire Event subscriber.

The characteristics of

⭐ Normally, we define a common function or event first and then call it. The publish-subscriber pattern is designed to decouple publishers and subscribers.

⭐ publish-subscriber pattern is a one-to-many relationship, that is, one scheduling center corresponds to multiple subscribers.

⭐ publish-subscriber mode has a Queue, or first in, first out. In JS, Array is used to simulate queues [Fn1, Fn2, fn3], defined first, executed first.

⭐ first define a message queue, need objects to subscribe. Instead of actively firing, objects are passively receiving.

An example to understand

Joe SAN, an ordinary programmer, goes to a bookstore to buy a book

Zhang SAN: Is there a little red book?

Shop assistant: No.

In an hour

Zhang SAN: Is there a little red book?

Shop assistant: No.

In an hour

Zhang SAN: Is there a little red book?

Shop assistant: No.

Ordinary programmers buy books, need to call the corresponding method frequently, this kind of polling method will undoubtedly increase the burden.

So how does a programmer publishing a subscriber model buy books?

Li Si, a programmer, went to a bookstore to buy a book

Li Si: Is there a little red book?

Shop assistant: No.

Li Si: I want to subscribe this book. When it is available, please give me a call and I will send you a message. If I get a book somewhere else, please unsubscribe.

In this example, the shop assistant belongs to the publisher, and Lisa belongs to the subscriber; Li Si registers the event of buying books with the dispatch center, and the clerk acts as the publisher. When a new book is released, the clerk releases the event to the dispatch center, and the dispatch center will send a message to Li Si in time.

Code demo

Publishing – subscriber model implementation ideas

πŸŒοΈβ™‚οΈ Creates a class.

πŸ„β™€οΈ creates a cache list (dispatch center) on this class.

πŸ€Ύβ™€οΈ there should be an on method to add the function fn to the cache list, that is, subscribers register events with the dispatch center.

πŸ€Έβ™€οΈ an EMIT method should fetch the event event type and execute the corresponding function in the cache list according to the event value. That is, the publisher publishes the event to the dispatch center, which processes the code.

πŸƒβ™€οΈ has an off method that unsubscribes based on the event event type.

The concrete realization of the idea

⭐ Parse the constructor

Based on the idea of implementing the publish-subscriber pattern, this class should look like this.

 /** * + attribute: message queue * {* 'click':[fn1,fn2,fn3], * 'mouse':[fn1,fn2,fn3] *} * + add content to message queue $on * + Delete content from message queue $off * + Trigger content from message queue $emit */
 
   class Observer {
    constructor() {
      this.message = {}
    };
    $on() {};
    $off() {};
    $emit() {};
  }
 
Copy the code

⭐ Analyze message queues

Referring to the code below, our message queue is an object with keys for what to delegate and values for what to do. Multiple operations can be performed, so it should be an array of functions.


  / A/news
  const handlerA = () = > {
    console.log('πŸš€ πŸš€ ~ handlerA');
  }
  B / / news
  const handlerB = () = > {
    console.log('πŸš€ πŸš€ ~ handlerB');
  }
  C / / news
  const handlerC = () = > {
    console.log('πŸš€ πŸš€ ~ handlerC');
  }
  // Use the constructor to create an instance
  const person1 = new Observer()
  
  // Delegate some content to person1 to watch for me (subscribe)
  // Execute messages A and B when there is A little Red Book
  person1.$on(The Little Red Book, handlerA)
  person1.$on(The Little Red Book, handlerB)
  // Execute messages B and C when Yellow Book is present
  person1.$on(The Yellow Book, handlerB)
  person1.$on(The Yellow Book, handlerC)

Copy the code

⭐ analyze the $on() method

  class Observer {
   / / subscriber
   $on(type, fn) {
      // Check whether this attribute exists
      // If not, initialize an empty array
      if (!this.message[type]) {
        this.message[type] = []
      }
      // Push a fn to the end of the array if there is one
      this.message[type].push(fn)
    };
  }
  
  person1.$on(The Little Red Book, handlerA)
  person1.$on(The Little Red Book, handlerB)
Copy the code

⭐ analyze the $off() method

$off() can unsubscribe a message or the entire subscription message queue.


 class Observer {
    constructor() {
      this.message = {}
    };
    // Unsubscribe
    $off(type, fn) {
      // Check to see if there is a subscription
      if (!this.message[type]) {
        return
      }
      // Check whether there is fn message
      if(! fn) {// Delete the entire message queue if there is no fn
        this.message[type] = undefined
        return
      }
      // Delete fn if there is fn
      this.message[type] = this.message[type].filter(item= >item ! == fn) }; }// The entire message queue does not need to be managed
  person1.$off(The Little Red Book)
  // The message queue still needs to be managed, but the handlerA message needs to be removed
  person1.$off(The Little Red Book, handlerA)
Copy the code

⭐ analyze the $emit() method

/ / publisher
$emit(type) {
  // Check to see if there is a subscription
  if (!this.message[type]) {
    return
  }
  // Execute the message loop (publish)
  this.message[type].forEach((item) = > {
    item()
  })
};

// Launch event
person1.$emit(The Little Red Book)
Copy the code

⭐ Full code

(() = > {
  class Observer {
    constructor() {
      // Message queue
      this.message = {}
    };
    $on(type, fn) {
      // Check whether this attribute exists
      // If not, initialize an empty array
      if (!this.message[type]) {
        this.message[type] = []
      }
      // Push a fn to the end of the array if there is one
      this.message[type].push(fn)
    };
    $off(type, fn) {
      // Check to see if there is a subscription
      if (!this.message[type]) {
        return
      }
      // Check whether there is fn message
      if(! fn) {// Delete the entire message queue if there is no fn
        this.message[type] = undefined
        return
      }
      // Delete fn if there is fn
      this.message[type] = this.message[type].filter(item= >item ! == fn) }; $emit(type) {// Check to see if there is a subscription
      if (!this.message[type]) {
        return
      }
      // Loop through the message
      this.message[type].forEach((item) = > {
        item()
      })
    };
  }
})()
Copy the code

Actual application scenario ToDoList

After we get a feel for the publish-subscriber model, let’s write an example to practice.

For example, choose the platitude ToDoList.

Analysis of the structure

Normally, we have a handleDom to manipulate the Dom; A handleData to manipulate data.

When we add a todo, we declare a handlerFn function that performs data manipulation and DOM manipulation in the function body.

function handlerFn() {
  // Operate data
  handleData();
  / / dom operation
  handleDom();
}
Copy the code

Or manipulate the DOM after manipulating the data.

// Operate data
function handleData() {
  / / dom operation
  handleDom();
}
Copy the code

Either way, these two approaches will result in coupling the manipulation of the data and the DOM. Next, we use the published subscriber model to decouple.

Publisher-subscriber model

We need three files, tododom.ts to manipulate dom; Todoevent. ts is used to manipulate data, and as you can see from the code below, there is no coupling between the two.

Todolist.ts is used to establish publishing subscribers and to connect data to the DOM.

todoDom.ts

import { ITodo } from './todoList';

class TodoDom {
  private oTodoList: HTMLElement;
  constructor(oTodoList: HTMLElement) {
    this.oTodoList = oTodoList;
  }
  // To generate an instance, pass in a DOM node
  public static create(oTodoList: HTMLElement) {
    return new TodoDom(oTodoList);
  }
  // Add to-do
  public addItem(todo: ITodo): Promise<void> {
    return new Promise((resolve, reject) = > {
      // Generate a node
      const oItem: HTMLElement = document.createElement('div');
      oItem.className = 'todo-item';
      oItem.innerHTML = this.todoView(todo);
      this.oTodoList.appendChild(oItem);
      resolve();
    });
  }
  // Remove backlog
  public removeItem(id: number): Promise<void> {
    return new Promise((resolve, reject) = > {
      // Get the to-do list
      const oItems: HTMLCollection = document.getElementsByClassName('todo-item');
      // Find by id
      Array.from(oItems).forEach((oItem) = > {
        const _id = Number.parseInt(oItem.querySelector('button').dataset.id);
        // Remove the corresponding DOM
        if(_id === id) { oItem.remove(); resolve(); }}); reject(); }); }// Change the todo status
  public toggleItem(id: number): Promise<void> {
    return new Promise((resolve, reject) = > {
      // Get the to-do list
      const oItems: HTMLCollection = document.getElementsByClassName('todo-item');
      // Find by id
      Array.from(oItems).forEach((oItem) = > {
        const oCheckBox: HTMLInputElement = oItem.querySelector('input');
        const _id = parseInt(oCheckBox.dataset.id);
        // Modify the corresponding DOM state
        if (_id === id) {
          const oContent: HTMLSpanElement = oItem.querySelector('span');
          oContent.style.textDecoration = oCheckBox.checked ? 'line-through' : 'none'; resolve(); }}); reject(); }); }// Insert the node
  private todoView({ id, content, completed }: ITodo): string {
    return `
    <input type="checkbox" ${completed ? 'checked' : ' '} data-id="${id}">
    <span style="text-decoration:${completed ? 'line-through' : 'none'}">${content}</span>
    <button data-id="${id}"></button>
    `; }}export default TodoDom;
Copy the code

todoEvent.ts

import { ITodo } from './todoList';

class TodoEvent {
  // An array of to-do lists
  private todoData: ITodo[] = [];
  // Generate an instance
  public static create(): TodoEvent {
    return new TodoEvent();
  }
  // Add a backlog
  public addTodo(todo: ITodo): Promise<ITodo> {
    return new Promise((resolve, reject) = > {
      // Find to do
      const _todo: ITodo = this.todoData.find((t) = > t.content === todo.content);
      // Return failure if there is already one
      if (_todo) {
        console.log('πŸš€πŸš€~ this item already exists');
        return reject(1001);
      }
      // Otherwise add a todo
      this.todoData.push(todo);
      console.log('πŸš€πŸš€~ Added successfully :'.this.todoData);
      resolve(todo);
    });
  }
  // Delete todo
  public removeTodo(id: number): Promise<number> {
    return new Promise((resolve, reject) = > {
      // Filter the backlog by id
      this.todoData = this.todoData.filter((t) = >t.id ! == id); resolve(id); }); }// Change the todo status
  public toggleTodo(id: number): Promise<number> {
    return new Promise((resolve, reject) = > {
      // Iterate over the to-do list array
      this.todoData = this.todoData.map((t) = > {
        // Find the corresponding ID and change the status
        if(t.id === id) { t.completed = ! t.completed; resolve(id); }returnt; }); }); }}export default TodoEvent;
Copy the code

todoList.ts

export interface ITodo {
  // id Unique identifier
  id: number;
  / / content
  content: string;
  // Whether to complete
  completed: boolean;
}

class TodoList {
  private oTodoList: HTMLElement;
  // Message queue
  private message: Object = {};
  constructor(oTodoList: HTMLElement) {
    this.oTodoList = oTodoList;
  }
  // Initialize the observer to expose the method, passing in a total DOM argument since we are manipulating the DOM
  public static create(oTodoList: HTMLElement) {
    return new TodoList(oTodoList);
  }

  public on(type: string, fn: Function) {
    // Check whether this attribute exists
    // If not, initialize an empty array
    if (!this.message[type]) {
      this.message[type] = [];
    }
    // Push a fn to the end of the array if there is one
    this.message[type].push(fn);
  }
  public off(type: string, fn: Function) {
    // Check to see if there is a subscription
    if (!this.message[type]) {
      return;
    }
    // Check whether there is fn message
    if(! fn) {// Delete the entire message queue if there is no fn
      this.message[type] = undefined;
      return;
    }
    // Delete fn if there is fn
    this.message[type] = this.message[type].filter((item: Function) = >item ! == fn); } public emit<T>(type: string,param: T) {
    // which Promise is executed
    let i: number = 0;
    // Array of functions to be executed
    let handlers: Function[];
    // Each time a single Promise is executed
    let res: Promise<unknown>;
    handlers = this.message[type];
    // Execute if the array length is not zero
    if (handlers.length) {
      / / Promise. Then form
      res = handlers[i](param);
      while (i < handlers.length - 1) {
        i++;
        res = res.then((param) = > {
          returnhandlers[i](param); }); }}}}export default TodoList;
Copy the code

index.js

Of course, you also need an entry file to get the program running.

//index.js
import type { ITodo } from './src/todoList';
import TodoList from './src/todoList';
import TodoEvent from './src/todoEvent';
import TodoDom from './src/todoDom';

((document) = > {
  // Get the corresponding node
  const oTodoList: HTMLElement = document.querySelector('.todo-list');
  const oAddBtn: HTMLElement = document.querySelector('.add-btn');
  const oInput: HTMLInputElement = document.querySelector('.todo-input input');
  // Create instances of three classes
  const todoList: TodoList = TodoList.create(oTodoList);
  const todoEvent: TodoEvent = TodoEvent.create();
  const todoDom: TodoDom = TodoDom.create(oTodoList);

  const init = (): void= > {
    // Subscribe to events
    todoList.on('add', todoEvent.addTodo.bind(todoEvent));
    todoList.on('add', todoDom.addItem.bind(todoDom));
    todoList.on('remove', todoEvent.removeTodo.bind(todoEvent));
    todoList.on('remove', todoDom.removeItem.bind(todoDom));
    todoList.on('toggle', todoEvent.toggleTodo.bind(todoEvent));
    todoList.on('toggle', todoDom.toggleItem.bind(todoDom));
    // Bind events
    bindEvent(todoList, oTodoList, oAddBtn, oInput);
    // Trigger the event
  };
  const bindEvent = (todoList: TodoList, list: HTMLElement, btn: HTMLElement, input: HTMLInputElement) = > {
    // Bind a click event to the add button
    btn.addEventListener(
      'click'.() = > {
        const val: string = input.value.trim();
        if(! val.length) {return;
        }
        todoList.emit<ITodo>('add', {
          id: new Date().getTime(),
          content: val,
          completed: false}); input.value =' ';
      },
      false
    );
    // Add a toggle status event to all checkboxes
    // Add a delete event for all delete buttons
    list.addEventListener(
      'click'.(e: MouseEvent) = > {
        const tar = e.target as HTMLLIElement;
        const targetName = tar.tagName.toLowerCase();
        if (targetName === 'input' || targetName === 'button') {
          const id: number = parseInt(tar.dataset.id);
          switch (targetName) {
            case 'input':
              todoList.emit<number>('toggle', id);
              break;
            case 'button':
              todoList.emit<number>('remove', id);
              break;
            default:
              break; }}},false); }; init(); }) (document);
Copy the code

At this point, a todoList mini-case based on the published subscriber model is complete.

So now you know why EventTarget. AddEventListener () is a sub? Does it look very similar to the on method we defined ourselves?

reference

The Core Design Pattern for the Web Front End: Publish Subscriber Pattern

Small partners feel helpful to you please like πŸ‘πŸ‘ support, feel good to write please pay attention to the column

πŸ‘‰πŸ‘‰ design patterns suitable for front-end personnel