The subscription-to-publish pattern is a common design pattern in the development world and is ubiquitous in our development. This time, let’s take a look at what’s not so mysterious.

What is the subscription publishing model

As the name implies, the subscribe and publish mode is divided into two actions: subscribe and publish. There are plenty of examples of subscription publishing in life, not just code.

Take li Jiaqi’s live broadcast on Double 11 as an example. For a damn discount, we searched Li Jiaqi’s live broadcast room on Taobao and found that it was not live time when we entered the page. At this time, there would be a broadcast reminder button on the page.

When we click on the broadcast alert, we are actually performing a subscription operation, subscribing to live notifications in the message center.

When the next live broadcast is scheduled, the message center will receive the broadcast event and then push notifications to all subscribers of the broadcast. That is, Taobao APP pushes news to you, “Li Jiaqi live began”, at this time you perform the corresponding operation, open Taobao, deliciously watch live shopping.

Subscription publishing code implementation

We used EventEmitter, which is common in interviews, as an example to easily implement a subscription publishing center

// Subscribe to the publication center: event emitter
class EventEmitter {
  constructor () {
    this.listeners = {};
  }

  // Subscribe to events
  addListener (event, cb) {
    if (this.listeners[event]) {
      this.listeners[event].push(cb)
    } else {
      this.listeners[event] = [].concat(cb)
    }
  }

  // Unsubscribe
  removeListener (event, cb) {
    const events = this.listeners[event];

    if (events) {
      const idx = events.indexOf(cb);

      if (idx > -1) {
        events.splice(idx, 1)}}}// Notification: execute event
  emit (event) {
    const events = this.listeners[event];

    if (events) {
      for (let i = 0, len = events.length; i < len; i++) { events[i](); }}}}const eventEmitter = new EventEmitter();

// Merchant subscription
eventEmitter.addListener('Li Jiaqi Live'.() = > {
  console.log('Merchant: Live streaming is underway. Load up.')})// User 2 subscribes
eventEmitter.addListener('Li Jiaqi Live'.() = > {
  console.log('User: Live streaming is going on')})// User 1 subscribes
eventEmitter.addListener('Li Jiaqi Live'.() = > {
  console.log('User: Live streaming is going on')})/ / release
eventEmitter.emit('Li Jiaqi Live')
Copy the code

Is there a sense of deja vu? Yes, the addEventListener that we often use on the front end is implemented based on the subscription publishing model.

Vue source subscription publishing implementation

We all know that anyone who asks how VUE works can throw around words like data hijacking subscription publishing. So how does VUE implement them? So let’s go on

The data was hijacked

Data hijacking is a responsive advance of vUE’s implementation of data by hijacking setter getter functions of data using Object.defineProperty

const observeObj = {
  name: 'xiao ming'
};

// Example: hijack the object's name attribute
let myname = null;

Object.defineProperty(observeObj, 'name', {
  configurable: true./ / can be configured
  enumerable: true./ / can be enumerated
  get () {
    console.log('Someone accessed observeObj. Name')

    // Return a variable I like
    return myname;
  },
  set (newVal) {
    console.log('Someone set observeObj. Name', newVal);

    myname = 'xiao'+ val; }})Copy the code

In doing so, we hijack the name property of observeObj, which is what we call data hijacking. When observeobj.name is called or assigned to observeobj.name, it calls the hijacking function get set that we defined.

With object.defineProperty, Vue now has the ability to listen for developers to modify data. So we often say that the principle of VUE is data hijacking, and that’s exactly what it is.

Set GET for multilevel objects

DefineProperty to see how a set get is triggered by a multilevel Object.

let deep = {};

const observeObj = {
  deep
};

Object.defineProperty(observeObj, 'deep', {
  get () {
    console.log('get deep')

    return deep
  },
  set (newVal) {
    console.log('set deep')

    deep = newVal
  }
})

Object.defineProperty(deep, 'name', {
  get () {
    console.log('get deep.name')
    return 'xiaoming';
  },
  set () {
    console.log('set deep.name')
  }
})

observeObj.deep.name = 2; // get deep set deep. Name Accesses the set of get name attributes of deep attributes
observeObj.deep.name = 2; // get deep set deep. Name Accesses the set of get name attributes of deep attributes
observeObj.deep.name; // get deep get deep. Name Accesses the get of the deep attribute
observeObj.deep = {}; // Access the set of deep
observeObj.deep.name; // Access deep get
Copy the code

From the above example we can conclude that

  1. When we set the chain properties, we actually access the properties in the chain in turngetAnd the trailing attributeset
object.deep1.deep2.name = 'x' // deep1 get -> deep2 get -> name set
Copy the code
  1. We set the object’sget setFunction to set a pointer to an address variable object. When we reset the object address, the previously set accessor is no longer accessible
let deep = {}

Object.defineProperty(deep, 'age', {
  get () {
    console.log('get deep.age')
    return 1;
  },
  set () {
    console.log('set deep.age')
  }
})

deep.age = 2 / / access set
// Change the pointing address
deep = {}
deep.age = 3 // Stop accessing set
Copy the code
  1. Even if you set the same value repeatedly, it will be accessedset
deep.age = 2 / / access set
deep.age = 2 // Access set +1
deep.age = 2 // Access set +1
Copy the code

Vue data responsive simulation

With the previous knowledge of subscription publishing and data hijacking in mind, let’s combine the two to simply simulate vUE’s data responsiveness

Let’s start by implementing our subscription publishing center Dep

// Store global subscribers
let targetWatch = null

// Subscribe to the release center
class Dep {
  constructor () {
    / / subscriber
    this.subs = [];
  }

  / / subscribe
  addSub (sub) {
    this.subs.push(sub)
  }

  // Issue a notification
  notify () {
    const subs = this.subs;

    for (let i = 0, len = subs.length; i < len; i++) {
      // Notify subscriberssubs[i].update(); }}}Copy the code

The subscriber Watcher is then implemented

class Watcher {
  constructor (expFn, cb) {
    // The callback function
    this.cb = cb;

    // Mark the current subscriber with a global variable
    targetWatch = this;

    // expFn is used to trigger subscription operations
    this.expFn = expFn;
    this.expFn();

    targetWatch = null;
  }

  update () {
    this.cb(); }}Copy the code

Now let’s look at the subscription action, where we have to do data hijacking to implement the monitor Observer

class Observer {
  constructor (data) {
    this.walk(data);
  }

  walk (obj) {
    // Iterate over object properties
    const keys = Object.keys(obj)

    for (let i = 0; i < keys.length; i++) {
      // Hijack for each key
      this.defineReactive(obj, keys[i])
    }
  }

  defineReactive (obj, key) {
    let val = obj[key];

    // Instantiate a unique Dep instance for each key
    const dep = new Dep();

    // If the key is an object, it needs to be recursively hijacked
    if (dep && typeof val === 'object') {
      new Observer(val)
    }

    // Data hijacker
    Object.defineProperty(obj, key, {
      get () {
        // Subscription publishing center triggers subscription deP to add current subscribers
        if (targetWatch) dep.addSub(targetWatch)
        return val;
      },

      set (newVal) {
        if(val ! == newVal) { val = newVal// Subscribe to the publication center trigger notificationdep.notify(); }})}}Copy the code

From the Observer defineReactive implementation we can see that subscribe to dep.addSub(targetWatch) and publish dep.notify().

So we’ve done that

  1. Subscribe to the release center Dep

  2. The subscriber Watcher

  3. The listener Observer

Now let’s take a look at how we can do data responsiveness with what we’ve implemented above, okay

// We need to listen to the data
const data = {
  name: ' '.deep: {
    age: 0}}// Add a listener for data
new Observe(data);

// Add subscribers
new Watcher(function expFn() {
  // Access the name attribute
  data.name;
  console.log('Here's the subscription function, subscribing to data.name',)},function cb() {
  console.log('Data updated, need to re-render the page', data.name)
})

// Add subscribers
new Watcher(function expFn() {
  // Access the deep property
  data.deep;
  console.log('Here is the subscription function, subscribing to data.deep',)},function cb() {
  console.log('Data updated, need to re-render the page', data.deep)
})

// Change the data
data.name = 'new' // Data update, need to re-render the page new
data.name = 'new2' // Data update, need to re-render the page new2
data.name = 'new3' // Data update, need to re-render the page new3
Copy the code

As you can see, through the above code, we have implemented automatic monitoring and subscription publishing of data. Let’s review this process again

  1. Implement subscription publishing center Dep

  2. Implement subscriber Watcher

  3. Implement listener Observer

  4. Instantiate the Observer with the listening target data as a parameter

  • Each attribute under data is pervaded recursively, performing data hijacking

  • Each attribute instantiates a subscription publishing center DEP

  1. Instantiate the subscriber Watcher
  • The first argument to watcher is the expFn accessor function, which accesses some property of data

  • When instantiated, the instance watcher is assigned to the global targetWatch in the constructor and expFn is called to access a property of data such as name

  • In the get function of name, where targetWatch has a value, the subscriber Watcher is added to the DEP corresponding to this attribute

  1. If the developer manually changes the data, such as data.name = new, the set function of the name attribute will be accessed. At this time, if the values before and after the judgment are different, the DEP will be notified of the data update

  2. The DEP receives notification of data updates in the property’s set function, iterating through the call to the subscriber Watcher’s update method

  3. Call CB in the update method to receive data update and get the latest data, so as to complete the next operation such as rendering

This is a simple implementation of vUE’s data listening and publish-subscribe mode. It’s actually written rather crudely, without arrays to be compatible and new values to be hijacked

But this is not the point, the point is that we can understand the implementation principle of VUE from it, then we will analyze the actual source code to understand the implementation of the whole VUE.

supplement

We looked at how object.defineProperty behaves at multiple levels earlier, but how does it behave in our DEMO

// We need to listen to the data
const data = {
  name: ' '.deep: {
    name: ' '}}// add add subscribers
new Watcher(function expFn() {
  // Access the deep.name property
  data.deep.name;
  console.log('Here is the subscription function, subscribing to data.deep.name',)},function cb() {
  console.log('Data updated, need to re-render the page', data.deep.name)
})

// Change the data
data.deep.name = 'new' // Data update, need to re-render the page new
data.deep = {
  name: 'new2'
} // we need to re-render the page new2
data.deep.name = 'new2'
Copy the code

When deep.name is modified, the subscriber is notified of the update.

Pointing data.deep to the new address is also notified, because we chained access multiple properties’ GET in expFn, and actually have multiple properties’ DEP added by subscriber watcher, So any changes to data.deep and data.deep.name will trigger a publish notification.

But changing data.deep. Name again at this point no longer triggers the update because the deep object of the name property we subscribed to has actually changed.

conclusion

This article briefly introduces the subscription publishing pattern and data hijacking, and implements an example of combining the two in VUE to achieve data responsiveness. In fact, it is quite messy, so I hope I can make do with it. This article is for the following vUE source code learning foundation, after the actual source code from the point of view of vUE data responsive implementation.