2
头图

Publish and subscribe

Publish and subscribe is one of the extremely basic and important design patterns. If you want to examine a design pattern in an interview, I think I will choose publish and subscribe without hesitation. So what exactly is publish and subscribe, and what scenarios does it apply to? When I started to learn this model, I was also confused. The big guys told me that the event binding in the front-end is a publish-subscribe (black question mark face). Yes, this is indeed, does this sentence sum up publish and subscribe?

To thoroughly learn and understand publishing and subscribing is not easy, but it is not difficult. People often say that reading a book a hundred times is just a matter of time. Many things are just a matter of time. Let's talk about my understanding of the publish and subscribe model in conjunction with my learning process.

Write a simple publish and subscribe

Release and subscription are of course divided into two parts: subscription and release, just like the official account you are following, you will receive his release only after you have subscribed. There must be a carrier, or a target, for subscription and publishing. Take the subscription publishing of this Web Reach is a carrier, all subscriptions. All of them should be stored somewhere in the official account.

Please pay attention to my public number: web Ruichi , send 1024 to get more front-end learning resources:

  1. Declare the carrier and put all subscribers in this carrier.
// 声明一个公众号作为载体
class Account {
  constructor(name) {
    // 这里的 name 没什么特殊含义,我的本意是指代当前 new 出来的公众号名
    this.name = name
    // 所有的订阅者都放在这个公众号的 subscribers 属性上
    this.subscribers = {}
  }
}
  1. The carrier is already there, and the next step is the process of adding subscriptions to the dispatch center
class Account {
  // 订阅过程,name 是订阅者的账号,fn 就代表订阅的事件
  // 订阅者的订阅事件可能不止一个,因此将事件作为一个数组
  // 考虑到可能重复订阅,因此也可以使用 set 作为订阅事件的容器
  subscribe(name, fn) {
    if(typeof fn !== 'function') return

    const subscribers = this.subscribers
    if(subscribers[name]) {
      // 去重
      !subscribers[name].includes(fn) && subscribers[name].push(fn)
    }else {
      subscribers[name] = [fn]
    }
  }
}
  1. Next is the release process
class Account {
  // 发布的过程可能只针对某些订阅者,比如是 a 用户发送了一条消息
  // 那公众号只对 a 进行回复,因此这里只对某一个订阅者做发布
  publish(name) {
    const subscribers = this.subscribers

    if(subscribers[name]) {
      subscribers[name].forEach(fn => fn())
    }
  }
}

At this point, the entire subscription release is finished. At present, we have implemented the most basic function subscription and publishing function, such as Zhang San subscribed to my official account, the implementation process is as follows:

const webRuichi = new Account('web瑞驰')

// 张三订阅
webRuichi.subscribe('张三', function() {
  console.log(`张三订阅了公众号`)
})

// 公众号给张三发布内容
webRuichi.publish('张三') // 输出-->  张三订阅了公众号

So far, our subscription publishing is ready to run, but there are still some shortcomings. Zhang San needs some special information when subscribing to content, and when the official account is Zhang San, he will send him what he needs when he publishes information. Next, make a modification to the publishing process:

class Account {
  publish(name, ...rest) {
    const subscribers = this.subscribers

    if(subscribers[name]) {
      subscribers[name].forEach(fn => fn(...rest))
    }
  }
}

Next, when Zhang San subscribes, he can tell the public what kind of information is needed later:

// 张三在订阅的时候想知道他订阅的是哪个公众号
webRuichi.subscribe('张三', function(name) {
  console.log(`张三订阅了 "${name}" 公众号`)
})

// 公众号在发布时告诉张三自己的公众号名
webRuichi.publish('张三', webRuichi.name) // 输出-->  张三订阅了 "web瑞驰" 公众号

The implementation of subscription publishing is very simple, that is, the events of the subscribers are placed in its own array, and the corresponding subscribers are retrieved when publishing to execute the events in sequence. When applied to the web front-end field, the most common is the binding of events. Subscribe to related events for a certain button during initialization (here, take the click event as an example), and make a release when it is triggered. This button is a carrier with events subscribed to by subscribers and published when the click event is triggered.

Everyone should be aware that subscription publishing is used for decoupling. As a beginner, why is he decoupled from subscribing to publishing? Isn't this the sequential execution of an array of functions, and it's also decoupled? Also, where is his real use? Continue to explore with these questions.

Where is publish and subscribe used

  1. Vue component passing parameters

I believe that everyone has encountered the vue of parameter transfer between components in 061277b4749726 in the interview. When the components are nested too deeply, the ordinary props parameter transfer will be very cumbersome. For example, component A needs to pass parameters to cousin B. The path will be like this: A component -> parent component -> grandpa component -> uncle component -> B component, there are three components before and after, which will cause pollution to the three-layer component, and in the later maintenance It also caused a lot of trouble. At this time, it will be very convenient to apply the publish and subscribe mode to pass parameters. Let's take a look at the code:

// main.js文件
import Vue from 'vue'
import Event from './event'   // 引入订阅发布

Vue.prototype.$event = new Event()

// A组件
<template>
  <div>
    <button @click="handleClick">传值</button>
  </div>
</template>
  
<script>
  export default {
    methods: {
      handleClick() {
        const passVal = '这是A组件想B组件传入的值'
        // 在点击的时候发布事件
        this.$event.publish('AToB', passVal)
      }
    }
  }
</script>

// B组件
<template>
  <div></div>
</template>
  
<script>
  export default {
    mounted() {
      // 订阅事件
      this.$event.subscribe('AToB')
    }
  }
</script>

It can be clearly seen from this example that publishing events in component A and subscribing to events in component B are completely separate. Changes in the logic published in component A will not affect the logic in component B. The previous props parameters are coupled to the middle three-tier components. Once the number of parameters to be passed is changed, the middle three-tier components must be changed accordingly.

  1. Subscription publication in asynchronous callback

Due to the javascript , the code is full of various asynchronous callbacks. For example, in a scenario in node: the browser requests the node server, and node wants to return the corresponding page, but the page needs to read the database and read the template file two I/O operations. If you use the traditional callback, it will be like this :

const fs = require('fs')
const queryMysql = require('./queryMysql')

fs.readFile('./template.ejs', 'utf8', (err, template) => {
  if(err) throw err
  
  queryMysql('SELECT * FROM user', (err, data) => {
    if(err) throw err
    
    render(template, data)
  })
})

Let's analyze the above code:

  1. The readability is very bad, and the implementation is not elegant enough
  2. There are two levels of callback nesting, and each level of callback must be handled with errors
  3. I/O that could be parallel has become serial, which is time-consuming in terms of performance

This is only two levels of nesting, and it will be extremely cumbersome if it is used in more asynchronous callbacks. In the development of asynchronous callbacks, there has been a way to simplify using publish and subscribe. Then we will use subscription publishing to improve the above code:

const fs = require('fs')
const Event = require('event')
const queryMysql = require('./queryMysql')

const eventEmitter = new Event()
// 这里使用闭包的形式返回订阅的事件,目的是使 html 成为局部变量
const genReadyEvent = () => {
  const html = {}
  const TOTAL_KEY_COUNT = 2  // 渲染模板使用的数据有两个
  
  return (key, data) => {
    html[key] = data
    if(Object.keys(html).length === TOTAL_KEY_COUNT) {
      render(html[template], html[data])
    }
  }
}
eventEmitter.subscribe('ready', genReadyEvent())

fs.readFile('./template.ejs', 'utf8', (err, template) => {
  if(err) throw err
  eventEmitter.publish('ready', 'template', template)
})

queryMysql('SELECT * FROM user', (err, data) => {
  if(err) throw err
  eventEmitter.publish('ready', 'data', data)
})

After the improvement of the above code, firstly, the readability of the code has been greatly improved, and the performance has also been improved, and the parallel I/O operation makes full use of the characteristics of node. In addition, if the rendering logic changes, readyEvent is generally changed, which is completely decoupled from the logic at the subscription desk.

The subscription release is perfected again

Through the above example, the implementation and application of subscription publishing are introduced. In fact, the current implementation still has some flaws, such as

  • User cannot unsubscribe
  • In some cases, you may only need to subscribe once
  • If the subscribed event cancels a subscription during the execution process
  • ...

For the above problems, we come to make solutions:

  1. Cancel subscription, need to provide a method to cancel subscription
class Account {
  unsubscribe(name, fn) {
    const subscribers = this.subscribers

    if(subscribers[name]) {
      // 如果没有提供对应的事件则将整个订阅全部移除
      if(!fn) {
        delete subscribers[name]
      }else if(typeof fn === 'function') {
        const index = subscribers[name].findIndex(event => event === fn)
        // 如果要移除的事件没有在订阅中则 index 为-1 (~按位非运算符)
        ~index && subscribers[name].splice(index, 1)
      }
    }
  }
}
  1. Unsubscribe after subscribing only once
class Account {
  subscribeOnce(name, fn) {
    if(typeof fn !== 'function') return
      
    const wrap = () => {
      fn()
      this.unsubscribe(name, fn)
    }
      
    this.subscribe(name, fn)
  }
}
  1. If an event in a subscription cancels subsequent events to be published at the time of publication, the problem of array collapse may occur during traversal, so here we make a modification to the subscribed event, so that the subscription is canceled Reconstruction will also be done at that time.
class Account {
  constructor(name) {
    this.name = name
    this.subscribers = {}
  }
  
  subscribe(name, fn) {
    if(typeof fn !== 'function') return
    
    const subscribers = this.subscribers
    if(subscribers[name]) {
      // 对订阅的事件进行包装
      const event = {
        hasRemoved: false,
        event: fn
      }
        
      subscribers[name].push(event)
    }else {
      const event = {
        hasRemoved: false,
        event: fn
      }
      
      subscribers[name] = [event]
    }
  }
  
  // 只订阅一次的代码不变
  
  // 取消订阅的代码也需要改造
  unsubscribe(name, fn) {
    const subscribers = this.subscribers

    if(subscribers[name]) {
      // 如果没有提供对应的事件则将整个订阅全部移除
      if(!fn) {
        delete subscribers[name]
      }else if(typeof fn === 'function') {
        const target = subscribers[name].find(eventInfo => eventInfo.event === fn)
        target && (target.hasRemoved = true)
      }
    }
  }
  
  // 发布的代码需要改造
  publish(name, ...rest) {
    const subscribers = this.subscribers
    const events = subscribers[name]
    if(events) {
      for(let i = 0, len = events.length; i < len; i++) {
        const eventInfo = events[i]
        if(eventInfo.hasRemoved) continue
        eventInfo.event(...rest)
        
        // 如果是将整个订阅事件移除后面就不用了继续发布了
        // 注意顺序,移除整个事件只可能是在某次发布之后,如果在之前已经全部移除了将不会执行到for内
        if(!subscribers[name]) break
      }
      
      // 发布完成后将取消订阅的事件移除
      if(subscribers[name]) {
        subscribers[name] = events.filter(eventInfo => !eventInfo.hasRemoved)
      }
    }
  }
}

The above is the realization of a complete subscription publication. In the actual call, a new object is required. Subscription and publication are scheduled (or managed) by this object. This object separates subscription and publication from each other so that they do not affect each other, which is the essence of decoupling.

Observer mode

The observer model is derived from subscription publishing, and it is also implemented based on subscription publishing. Subscription publishing is to publish on demand, and the state of the observing object will be updated synchronously when the state of the observing object changes.

In addition, the observer mode is a one-to-many dependency relationship. The observer can be observed by multiple observers. When the state of the observer is updated, the observer is notified, and then the observer updates the state synchronously. According to this, let's implement the observer mode:

// 首先应该有一个被观察者
class Subject {
  constructor(name, state) {
    this.name = name
    this.state = state
 
    this.observers = []
  }
 
  // 添加观察者
  addObserver(ob) {
    this.observers.push(ob)
  }
 
  // 删除观察者
  removeObserver(ob) {
    const observers = this.observers
    const index = observers.indexOf(ob)
    ~index && observers.splice(index, 1)
  }
 
  // 被观察者的状态发生变化后通知观察者
  setState(newState) {
    if(this.state !== newState) {
      const oldState = this.state
      this.state = newState
      this.notify(oldState)
    }
  }
 
  // 通知所有观察者被观察者状态变化
  notify(oldState) {
    this.observers.forEach(ob => ob.update(this, oldState))
  }
}
 
// 之后有一个观察者
class Observer {
  constructor(name) {
    this.name = name
  }
 
  update(sub, oldState) {
    console.log(`${this.name}观察的${sub.name}的状态由 ${oldState} 变化为 ${sub.state}`)
  }
}
 
const sub = new Subject('小baby', '开心的笑')
const father = new Observer('爸爸')
const mother = new Observer('妈妈')
sub.addObserver(father)
sub.addObserver(mother)
sub.setState('悲伤的哭')

As can be seen from the above code, the observer and the observed are loosely coupled, and they can be changed independently. One observer can be observed by multiple observers, and the observer can decide which observer to observe by himself.

Of course, we can also store which observers the current observer observes in the observer. In this way, when the observer sends a broadcast notification, the observer can also freely decide whether or not to respond to a certain observer. To process:

class Subject {  addObserver(ob) {    this.observers.push(ob)    // 观察者添加当前的观察对象(也就是被观察者)    ob.addSub(this)  }} class Observer {  constructor(name) {    this.name = name    // 当前的观察对象(被观察者)    this.subs = []    this.excludeSubs = []  }   // 添加当前观察的对象  addSub(sub) {    this.subs.push(sub)  }   // 添加不进行处理的观察者  addExcludeSubs(sub) {    this.excludeSubs.push(sub)  }   update(sub, oldState) {    if(this.excludeSubs.includes(sub)) return     // do something ...      }}

The responsive principle of vue uses the observer mode. In Object.definePerporty , an observer is added to each attribute, and the observer is notified in the set method that the state of the attribute has changed. Therefore, the watcher will be triggered to execute again after the data change. _update method on the vue prototype to generate a new virtual DOM, and continue to call the render method for DOM-diff to update the page.

Summarize

This article compares the subscription publishing and the observer model. Subscription publishing connects subscribers and publishers through a carrier (scheduling center), and subscribers and publishers are independent of each other, so as to achieve the purpose of decoupling.

In the observer mode, the observer and the observed are loosely coupled, and the whole process is scheduled by the observed. The observed can freely decide which observer is to observe, and the observer can also decide whether to process an observed. When the state of the observer changes, the observer will be actively notified, and the observer will make the corresponding treatment. in addition:

If there is any improper expression or error in the knowledge points in the article, please leave a message to criticize and correct, and make progress together!


web瑞驰
46 声望3 粉丝

一枚自学前端的螺丝机