I was exposed to the publish-subscribe model in my recent work study. The idea of programming in the application is also very extensive, for example in Vue also a large number of use of the design pattern, so will be combined with Vue source code and you talk about their shallow understanding.

What is the main content of the publish subscribe model?

  1. Publish a function that executes the corresponding callback when it publishes
  2. Subscribing functions, adding subscribers, passing in functions to be executed when publishing, possibly with additional arguments
  3. A list of cached subscribers and their callback functions
  4. Unsubscribe (need to discuss case by case)

In this way, just like the event model in JavaScript, we bind the event function to the DOM node, and when triggered, we apply the publish-subscribe mode.

Let’s implement one of the aboveObserverThe objects are as follows:

// The key-value pair used to store the event name of the subscription and the list of callback functionsfunction Observer() {this.cache = {}} //key: identifies the type of the subscription message. Observer.prototype.on =function (key,fn) {
    if(! This.cache [key]){this.cache[key]=[]} this.cache[key].push(fn)} //argumentsfunction (key) {
    if(this.cache[key]&&this.cache[key].length>0){
        var fns = this.cache[key]
    }
    for(leti=0; i<fns.length; I++) {Array. The prototype. The shift. The call (the arguments) FNS [I] apply (this, the arguments)}} / / remove the need to pay attention to, if you directly into an anonymous function fn, When you remove is unable to find the function and remove it, flexible way is introduced to a / / pointer to the function, it was this pointer which feeds into the Observer. The prototype. Remove =function (key,fn) {
    let fns = this.cache[key]
    if(! fns||fns.length===0){return} // If fn is not passed in, then all subscriptions for the event are unsubscribedif(! fn){ fns=[] }else {
        fns.forEach((item,index)=>{
            if(item===fn){
                fns.splice(index,1)
            }
        })
    }
}


//example


var obj = new Observer()
obj.on('hello'.function (a,b) {
    console.log(a,b)
})
obj.emit('hello'// The callback to the unsubscribe event must be the named function obj.on('test',fn1 =function () {
    console.log('fn1')
})
obj.on('test',fn2 = function () {
    console.log('fn2')
})
obj.remove('test',fn1)
obj.emit('test')

Copy the code

Why publish and subscribe? Its advantages are:

  1. Achieve temporal decoupling (asynchronous communication between components, modules)
  2. Decoupling between objects, where publish-subscribed objects manage the coupling between objects.

The publish-subscribe model is inVueThe application of

  1. VueApplication of instance methods in :(Current version :2.5.16)
  • Document portal
  • Source code portal
  • (Introduced flow.js for static type checking)
// vm.$on
export functionEventsMixin (Vue: Class<Component>) {const hookRE = /^hook:/ // / Var. Prototype$on = function(event: string | Array < string >, fn: Function) : Component {const vm: Component = this / / incoming type into an Arrayif (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                this.$on(event[I], fn) // Recurse and pass the appropriate callback}}else{ // (vm._events[event] || (vm._events[event] = [])).push(fn) // optimize hook:event cost by using a boolean flag marked  at registration // instead of ahash lookup
            if (hookRE.test(event)) {
                vm._hasHookEvent = true}}return vm
    }


// vm.$emit

 Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if(process.env.NODE_ENV ! = ='production') {
      const lowerCaseEvent = event.toLowerCase()
      if(lowerCaseEvent ! == event && vm._events[lowerCaseEvent]) { tip( `Event"${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}". `)}}let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      for (leti = 0, l = cbs.length; i < l; I ++) {try {CBS [I]. Apply (vm, args)// execute the previously passed callback} catch (e) {handleError(e, vm, 'event handlerfor "${event}"`)}}}return vm
  }

Copy the code

Vue also implements vm.$once (listen once); And vm.$off (unsubscribe), you can see how this is done in the same file.

  1. VueApplication of data update mechanism
  • Observer A property of each object added to the subscriber container Dependency(Dep) to notice when data changes.
  • Watcher: Listener/subscriber to a property data that notifies the directive to recompile the template and render the UI if the data changes
  • Source: source portal – Observer
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type*/ / Walk (obj: Object) {const keys = object.keys (obj)for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code
  • Dep object: The subscriber container that maintains the Watcher source portal
exportdefault class Dep { static target: ? Watcher; id: number; subs: Array<Watcher>;constructor() {this.id = uid++ this.subs = [] // store subscribers} // add watcher addSub (sub: Watcher) {this.subs.push(sub)} // Remove removeSub (sub) {remove(this.subs, sub)}depend () {
    if(dep.target) {dep.target.adddep (this)}} // Change notificationnotify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Copy the code

Examples of small applications at work

  1. Scenario: Wepy-based applets. Because the project itself is not complex enough to use the suppliedreduxHowever, there are asynchronous operations associated between different components (not just parent and child components). So an Observer object that was implemented at the beginning of this article is mounted on the Wepy object. Bus mechanism as part of communication between components:
wepy.$bus= new Observer() // You can then subscribe and publish messages in different modules and componentsCopy the code

Points to pay attention to

Of course, there are downsides to the publish-subscribe model.

  1. Creating a subscriber itself consumes memory. After subscribing to a message, perhaps, there will never be a publication, and the subscriber will always be in memory.
  2. While objects are decoupled, their relationships can be buried deep behind the code, which incurs maintenance costs.

Of course, design patterns exist to help us solve scenario-specific problems, and learning to use them in the right scenario is the most important.

Spread the word

This article is published in Front of Mint Weekly. Welcome to Watch & Star.

Welcome to the discussion. Give it a thumbs up before we go. ◕ ‿ ◕. ~