1. Introduction

Recently, I have been combing the principle of VUE responsiveness, and I have some experiences, which are worth writing a blog. In fact, I have tried to understand the principle of VUE responsiveness before. After all, if you use VUE in the interview, you will basically be asked a few words about the principle of VUE responsiveness. In the past, this piece of learning is to look at the articles written by others, or to turn over the source code. In this process, I found that after reading a lot of articles, a sentence summary is that vue intercepts the getter/setter of data through object.defineProperty or Proxy API, and then makes the relevant logic of data response in the getter/setter. Nothing else. At the same time, I found that the time spent looking directly at the source code was too small compared to the harvest. Of course, it is difficult to see the source code or the current level is not enough to see the source code that step. Later I see a year on zhihu front end do not understand front end framework source code to do? This question, and what is Yuyuxi’s experience of maintaining a large open source project? The answer to this question. The following enlightenments are summarized:

  1. Pay attention to basic capabilities such as data structures, algorithms, and design patterns. The lack of these basic capabilities often leads us to not understand the ideas of the source authors. At the same time, improving these basic capabilities can also raise our ceiling.
  2. Start with some function point, don’t start with the entry file. For example, Vue, I am more interested in its responsive principle, I will specifically look at responsive related source code and articles.
  3. Don’t try to understand every line of code in the source code, but first get the idea. The source code is so detailed that getting caught up in it often makes reading the source code extremely difficult. The most important thing is to learn what the source code does in order to do something, without necessarily knowing what each line of the source code does.
  4. It’s better to do it once than watch it a hundred times. Sometimes wait for light to see the source code and articles, not necessarily how much harvest, or read and forget. In fact, according to their own understanding or ideas to implement it will be better. The specific implementation details do not have to be aligned with the source code, and the function can be simple. I believe doing it by myself will help us understand the source code more efficiently.

The key to Vue responsiveness is the data hijacking and the publish/subscribe pattern. The purpose of data hijacking is to implement publish/subscribe, through which each “subscriber” is notified when data changes occur, i.e., the various template syntax, computed attributes, listeners, and so on in Vue. Let’s start with a few concepts related to publish and subscribe, and then introduce the simple responsive models of Vue2.0 and Vue3.0 by implementing a simple version of Vue yourself. Note that this is a simple model, but it should help you in most interviews. The code used in this article is on GitHub

2. Observer mode

2.1 concept

The observer mode is a notification mechanism that allows the sender (the observed) and the receiver (the observer) to be separated from each other. There is a difference between the observer pattern and the publish/subscribe pattern, which will be discussed in more detail later.

2.2 the sample

Here is a simple example of the observer pattern, where the business logic is to notify customers when a new item is available in the store. For convenience, this example can be run directly through Node without any UI implementation concerns.

// The observed
module.exports = class Store {
  constructor() {
    // The list of products
    this.products = new Set(a)// The list of observers
    this.observers = []
  }

  // Registered observer
  addObserver(watcher) {
    this.observers.push(watcher)
  }

  // Notify observers of new items
  addProduct(name) {
    if (this.products.has(name)) return

    this.products.add(name)

	// Iterate through the list of observers and call the corresponding processing method for the observer
    this.observers.forEach((watcher) = > watcher.onPublished(name))
  }
}
Copy the code
/ / observer
module.exports = class watcher {
  constructor(name) {
    this.name = name
  }

  onPublished(product) {
    console.log(` observerThe ${this.name}Observed commodity${product}Shelves `)}}Copy the code
// Notify customers when a new product is on sale
const Store = require('./store')
const Watcher = require('./watcher')

const supermarket = new Store()
const watchA = new Watcher('A')
const watchB = new Watcher('B')

supermarket.addObserver(watchA)
supermarket.addObserver(watchB)

supermarket.addProduct('banana')

setTimeout(() = > {
  supermarket.addProduct('apple')},3000)
Copy the code

An obvious feature of the observer pattern is that the observer maintains a list of observers, which means that the observer knows which observers are watching him. When notification is required, iterate through the list and notify observers.

3. Publish and subscribe

3.1 Difference from observer mode

Refer to the above example of the observer mode, in which the observed actively collects the observers. In the publism-subscription model, the publisher does not need to collect subscribers actively, and the subscribers subscribe to the publishing platform. The publisher pushes the changes to the publishing platform, and the publishing platform informs the subscribers.

3.2 For example

I saw a good analogy on GitHub:

The publish-subscription model is like the relationship between the newspaper, the post office and the individual. The subscription and distribution of the newspaper are done by the post office. The newspaper only delivers the papers to the post office.

The observer model is like the relationship between the individual dairy farmer and the individual. Dairy farmers are responsible for keeping track of how many people have ordered the product, so each individual has the same method of getting milk. The farmer calls this method when he has new milk.

3.3 the sample

Based on the above metaphor, we implement a simple project in which readers subscribe to newspapers through the post office

// A publisher is a newspaper
module.exports = class Publisher {
  constructor() {
    this.subscribers = []
  }

  addSubscriber(subscriber) {
    this.subscribers.push(subscriber)
  }

  publish(value) {
    this.subscribers.forEach((subscribe) = >{
		subscribe.update(value)
	})
  }
}
Copy the code
// The subscriber is the reader
module.exports = class Subscriber {
  constructor(cb) {
    this.cb = cb
  }

  update(val) {
    this.cb(val)
  }

  // Subscribers actively subscribe to the publishing platform
  subscribe(publisher) {
    publisher.addSubscriber(this)}}Copy the code
// The publishing platform is the post office
module.exports = class Publisher {
  constructor() {
    this.subscribers = []
  }

  addSubscriber(subscriber) {
    this.subscribers.push(subscriber)
  }

  publish(value) {
    this.subscribers.forEach((subscribe) = >{
		subscribe.update(value)
	})
  }
}
Copy the code
// Subscribe to the newspaper through the post office
const Producer = require('./producer')
const Publisher = require('./publisher')
const Subscriber = require('./subscriber')

// Create a newspaper
const newspaper = new Producer()

// Create two post offices to distribute newspapers
const postOfficeA = new Publisher()
const postOfficeB = new Publisher()

// Create three readers and subscribe to the paper from different newspapers
const readerA = new Subscriber((value) = > {
  console.log('Reader A received${value}`)})const readerB = new Subscriber((value) = > {
  console.log('Reader B received${value}`)})const readerC = new Subscriber((value) = > {
  console.log('Reader C received${value}`)
})

readerA.subscribe(postOfficeA)
readerA.subscribe(postOfficeB)
readerB.subscribe(postOfficeB)
readerC.subscribe(postOfficeB)

newspaper.addPublisher(postOfficeA)
newspaper.addPublisher(postOfficeB)

newspaper.publish('New Youth')
Copy the code

As you can see from the above code, the biggest difference between the publish-subscribe pattern and the Observer pattern is the addition of a publishing platform. In addition, publishers do not actively collect subscribers, but rather specify the publishing platform. Subscribers are also required to register themselves with the publishing platform, that is, to add themselves to a list of subscribers maintained within the publishing platform. Instead of the publishing platform actively collecting subscribers, subscribers call the publishing platform’s corresponding methods to register themselves with the publishing platform. When a publisher changes, it first notifies the publishing platform, which then traverses its subscriber list and informs the subscribers of the change.

4. Simple models

4.1 Publish and subscribe logic

After understanding the Observer pattern, as well as the publish/subscribe pattern, we can understand the basic Vue responsive model. Here’s a picture on Vue’s website:

This diagram illustrates the logic of publish and subscribe, which is distilled into three roles:

  • The data declared in the publisher Vue
  • The template syntax, computational properties, listeners, and so on in the subscriber Vue are all subscribers, and subscribers typically have mechanisms similar to “callback functions” that consume data in the changed callback function to provide a way to handle publisher changes
  • Each property in the publishing platform data has a corresponding publishing platform and the publishing platform collects the subscribers that use that property and informs the subscribers when the publisher sends the change that the property has changed

There are three key problems in the linkage of the three roles:

  • How to create a publishing platform for each property of data: create a publishing platform for each key by traversing the key of data
  • How the publishing platform collects subscribers: 1. When a subscriber is created, its change callback is called, triggering getters for the properties used. 2. The publishing platform maintains a singleton dep. depTarget. The subscriber points to the dep. depTarget before calling the callback function and sets the dep. depTarget to null after the callback. 3. Publishing platform through Object beforehand. DefineProperty/Proxy API to hijack the get method, when the attribute is the get the Dep. DepTarget publishing platform is added to the attributes of the subscriber list, This procedure determines if dep.depTarget is empty, if the pointed subscriber is already in the list, and so on.
  • Publishing platform how to obtain a publisher sent changes: publishing platform in advance by the Object. The defineProperty/Proxy API hijacked set method, call the attributes in the set method publishing platform subscriber list of each subscriber in the process

4.2 simple Vue2.0 responsive model

As stated in the preface, you can better understand the source code by implementing it yourself. Here is a simple Object. DefineProperty to achieve monitoring data update and refresh the page. First create an HTML file and a JS file

<! -- index.html -->
<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Lightweight vue2.0 implementation</title>
</head>
<body>
  <div id="app"></div>
  
  <script type="module" src="./index.js"></script>
</body>
</html>
Copy the code
// index.js
import Vue from './src/vue.js'

const App = new Vue({
  el: '#app'.data() {
    return {
      name: 'Trump'.info: {
        message: 'No one knows better than him.',}}},render(createElement) {
    return createElement(
      'div',
      [
        createElement('span'.`The ${this._data.name}Said:The ${this._data.info.message}`)])}})setTimeout(() = > {
  App._data.name = 'sichuan treasure'
}, 2000)

setTimeout(() = > {
  App._data.info.message = 'MAGA!!!! '
}, 4000)
Copy the code

In index.js we create a Vue instance of our own implementation, hang it on the node with the ID app, and then implement the code in./ SRC /vue.js

// ./src/vue.js
// Introduce a function to handle data hijacking
import observe from './observer.js'
// Introduce the observer
import Watcher from './watcher.js'

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data()
    this.render = options.render
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el

	// Data hijacking
    observe(this._data)

    // Create a subscriber to subscribe to the _data changes
    // Re-render the component when the subscriber receives the change notification
    new Watcher(this._data, () = > {
      this.$mount()
    })
  }

  // This is where the HTML node is created
  createElement(tagName, children) {
    let element = document.createElement(tagName)

    if (Object.prototype.toString.call(children) === '[object Array]') {
      children.forEach((child) = > {
        element.appendChild(child)
      })
    } else {
      element.textContent = children
    }

    return element
  }

  // Create and mount a node
  $mount() {
    const elements = this.render(this.createElement)
    this.$el.innerHTML = ' '
    this.$el.appendChild(elements)
  }
}

export default Vue

Copy the code

Next comes the key way to deal with data hijacking

// ./src/observer.js
import Dep from './dep.js'

const typeTo = (val) = > Object.prototype.toString.call(val)

// Override the get/set attribute method
function defineReactive(obj, key, val) {
  // Each object property has a Dep that acts as a publishing platform for changes to that property
  let dep = new Dep()

  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.// Get when the publishing platform collects subscribers
    get() {
      console.log(`get ${key}`)

      if (Dep.depTarget && Dep.depTarget.id >= 0) {
        console.log('Current subscriber ID:${Dep.depTarget.id}`)
      }

      dep.addSub(Dep.depTarget)

      return val
    },
    // Set when publishing platform DEP notifying subscribers
    set(newValue) {
      console.log(`set ${key}`)

      if (newValue === val) return

      val = newValue
      dep.notify()
    },
  })
}

function walk(obj) {
  Object.keys(obj).forEach((key) = > {
    // If the value is an object, continue processing the fields inside the object
    if(typeTo(obj[key]) === '[object Object]'){
      walk(obj[key])
    }

    // Process the attribute itself
    defineReactive(obj, key, obj[key])
  })
}

// observe is used to hijack data
function observe(obj) {
  if(typeTo(obj) ! = ='[object Object]') {
    return null
  }

  walk(obj)
}

export default observe
Copy the code

In./ SRC /observer.js, dep.js is introduced, which is the publishing platform class in the publisher-subscribe model. Create a publishing platform for each key by iterating through the keys of the object in observer.js. When the property GET is triggered, it proves that a subscriber is using the property, and dep.depTarget points to the subscriber using the property, adding it to the publishing platform’s subscriber list. When the property is set, the set is triggered, and the publishing platform traverses the subscriber list, notifying the subscriber. Look at the implementation of./ SRC /dep.js

// ./src/dep.js
// Publish platform in the publish/subscribe model
class Dep{
  constructor() {
    // List of subscribers
    this.subs = []
  }

  addSub(sub) {
  	// If the subscriber is not on the subscriber list, add it
    if(sub && (this.subs.indexOf(sub) === -1)) {
      this.subs.push(sub)
    }
  }

  notify() {
    console.log('Notify changes'.this.subs.length)
    this.subs.length > 0 && this.subs.forEach((sub) = > {
      sub.update()
    })
  }
}

Dep.depTarget = null

export default Dep

Copy the code

This part of the code is easy to understand. The rest is the implementation of.src /watcher.js

// ./src/watcher.js
import Dep from './dep.js'

// Distinguish subscribers by id
let id = 0

class Watcher{
  // At subscriber creation, point the singleton dep. depTarget to the current subscriber
  constructor(value, cb) {
    this.cb = cb

    // The purpose of calling the get method at creation time is primarily to trigger the GET of the used property by calling cb
    // Then add the subscriber to the publishing platform of the corresponding attribute
    this.get()

    // this. Val points to vue._data
    this.val = value

    // The id determines whether it is the same subscriber
    this.id = ++id
  }

  get() {
    Dep.depTarget = this

    this.cb()

    // The singleton is reset after the publishing platform collects subscribers
    Dep.depTarget = null
  }

  // The subscriber calls this.cb() on the update to trigger the get of the used property
  // If the subscriber is not in the list of subscribers for the corresponding property publishing platform, it will be added to the list
  update() {
    this.get()

    console.log('val value'.this.val.name, this.val.info.message)
  }
}

export default Watcher

Copy the code

The simplified version of Vue2.0 is now available in the browser

4.2 Simple Vue3.0 responsive model

/ SRC /observer.js uses Proxy API to hijack data. Because Proxy API returns a Proxy object, index.js and./ SRC/viee. js are written differently in some ways

// ./src/observer.js 
// Use proxy API for data hijacking
import Dep from './dep.js'

const typeTo = (val) = > Object.prototype.toString.call(val)

// Override the get/set attribute method
function defineReactive(obj) {
  // Each object property has a Dep that acts as a publishing platform for changes to that property
  let dep = new Dep()

  if(typeTo(obj) ! = ='[object Object]') {
      return null
  }

  return new Proxy(obj, {
    get(target, key) {
      console.log('trigger get', target, key)

      dep.addSub(Dep.depTarget)

      return target[key]
    },
    set(target, key, value, receiver) {
      console.log('the trigger set', target, key, value)

      let newValue = Reflect.set(target, key, value, receiver)

      dep.notify()

      return true}})}function walk(obj) {
  const res = {}

  Object.keys(obj).forEach((key) = > {
    if (typeTo(obj[key]) === '[object Object]') {
      // If the value is an object, continue processing the fields inside the object
      res[key] = walk(obj[key])
    } else {
      // If it is not an object, then the value is assigned
      res[key] = obj[key]
    }
  })

  // Remember to process the property itself
  return defineReactive(res)
}

// observe is used to hijack data
function observe(obj) {
  if(typeTo(obj) ! = ='[object Object]') {
    return null
  }

  return walk(obj)
}

export default observe

Copy the code
// ./src/vue.js 
import observe from './observer.js'
import Watcher from './watcher.js'

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data()
    this.render = options.render
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el

	// This is different from 2.0
    this.$data = observe(this._data)

    // Create a subscriber to subscribe to the _data changes
    // Re-render the component when the subscriber receives the change notification
    new Watcher(this.$data, () = > {
      this.$mount()
    })
  }

  createElement(tagName, children) {
    let element = document.createElement(tagName)

    if (Object.prototype.toString.call(children) === '[object Array]') {
      children.forEach((child) = > {
        element.appendChild(child)
      })
    } else {
      element.textContent = children
    }

    return element
  }

  $mount() {
    const elements = this.render(this.createElement)
    this.$el.innerHTML = ' '
    this.$el.appendChild(elements)
  }
}

export default Vue
Copy the code
// index.js
import Vue from './src/vue.js'

const App = new Vue({
  el: '#app'.data() {
    return {
      name: 'Trump'.info: {
        message: 'No one knows better than him.',}}},render(createElement) {
    return createElement(
      'div',
      [
        createElement('span'.`The ${this.$data.name}Said:The ${this.$data.info.message}`)])}})setTimeout(() = > {
  // Use the $data attribute to manipulate the object returned by the Proxy API
  App.$data.name = 'sichuan treasure'
}, 2000)

setTimeout(() = > {
  App.$data.info.message = 'MAGA!!!! '
}, 4000)

Copy the code

The result is the same as 2.0

End of the 5.

This only shows the simplest model of Vue responsiveness, which is certainly very different from the source code in terms of details and functionality, but with an understanding of the publish/subscribe model and my own simple Vue2.0 and 3.0 models, the interviewer will not be too afraid to ask again.

6. Reference

【 VUE Series 】 From publish and subscribe pattern interpretation, to vUE responsive principle implementation (including VUE 3.0)

The observer

What is the difference between the observer pattern and the subscription-publish pattern