preface

This is also a result of blogging. I had messed around with a template engine when I was writing a blog, and I thought I would optimize it later and put in a Vue responsive principle to make it a small “framework” that would reduce the amount of component-based front-end code I needed to write a blog. This is just a small project for myself to learn the responsive principle of Vue2.0. The implementation of the code may be very preliminary, and some places will be poor, please forgive me. This article picks up where we left off with the implementation of reactive.

Project source: Portal implementation effect: My blog is transformed with this small framework

What is the MVVM pattern[1][2]?

Model-view-viewmodel abstracts the state and behavior of the View, allowing us to separate the UI from the business logic. Of course the ViewModel already does this for us, fetching the data from the Model and helping with the business logic involved in displaying content in the View.

The MVVM pattern is composed of the following three core components, each with its own unique role:

  • Model – A data Model that contains business and validation logic
  • View – Defines the structure, layout, and appearance of the View on the screen
  • ViewModel – Act as the emissary between “View” and “Model”, helping to process all the business logic of View, connecting the View layer and Model layer through two-way data binding.

The responsive principle of Vue2.0

How is vUE’s responsiveness implemented? Object.defineproperty is the key. It can be used to convert all incoming VUE instance properties into getters/setters. Something like this:

function defineReactive(obj, key, value) {
  let val = value
  Object.defineProperty(obj, key, {
    enumerable: true./ / can be enumerated
    configurable: true.// The property descriptor can be changed and deleted from the corresponding object only if and if the property is configured differently. The default is false.
    get: function () {
      / / val closures
      return val
    },
    set: function (newVal) {
      // Val is always in the closure, and after this is set, the latest value will be retrieved when we get it
      val = newVal
    }
  })
}
Copy the code

We can listen when obJ pairs change the key value. So how to implement the relevant operation after the data changes?

Vue’s responsiveness consists of three major parts: Observer, Dep and Watcher.

  • Observer: places each target object (i.edata) to convert togetter/setterForm for dependency collection and scheduling updates. For each object in reactive data, there is oneObserverAn instance of the
  • Dep:ObserverThe instancedataIn the triggergetterWhen,DepThe instance will collect dependenciesWatcherInstance,DepAn instance is an administrator that can manage more than oneWatcherInstance, whendataWhen it changes, it passesDepInstance toWatcherInstance sends notifications for updates. One property for each object in reactive dataDepAn instance of the
  • Watcher: is an observer object. After relying on collectionWatcherObjects can beDepInstance management, data changesDepThe instance will notifyWatcherInstance, and then fromWatcherInstance to update the view.

The idea of responsive implementation

An observe function is passed in as data and returns an Observer instance. The flow of the observe function is:

  1. Checks whether the passed object is an array or an object, otherwise returnsundefined
  2. If so, it determines whether it is already responsive, and if so, it returns what already existsObserverInstance, otherwise instantiate a new oneObserverInstance and return.
/** * listen * @param {Object} data */
export default function observe(data) {
  if (typeofdata ! = ='object' || data === null) {
    // Not objects or arrays
    return
  }
  let ob
  if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observer) {
    ob = data.__ob__ //__ob__ is an Observer instance of defined responsive data
  } else {
    ob = new Observer(data)
  }
  return ob
}
Copy the code

ObserverThe realization of the class

So let’s look at the Observer class definition:

class Observer {
  constructor(data) {
    this.data = data // Initialize the property
    this.dep = new Dep()// Initialize the deP instance, which is used in array listening
    def(data, '__ob__'.this) // Add a property to the object itself pointing to the responsive object to determine whether the data has become responsive and can be found through the data object
    if (Array.isArray(data)) {// Check whether it is an array
      if ('__proto__' in {}) {
        data.__proto__ = arrProto
      } else {
        addSelfMethod(data, arrProto, arrayKeys)
      }
      this.observeArr(data)
    } else {// If the object is listening on the object
      this.observeObj(data)
    }
  }
// Listen to the object
  observeObj(obj) { 
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
  // Listen for arrays
  observeArr(arr) {
    for (let i = 0; i < arr.length; i++) {
      observe(arr[i])
    }
  }
}
Copy the code

From the definition, there are several key points in the instantiation of the Observer class: instance attribute DEP, array listener, and object listener, so let’s look at the implementation of the DEP class.

DepThe realization of the class

export default class Dep {
  constructor() {
    this.subs = [] // Array to manage the Watcher instance
  }
  addSub(sub) { // Add the Watcher instance
    this.subs.push(sub)
  }
  notify(isArrayMethod = false) { // Call the update method of the Watcher instance
    this.subs.forEach(function (sub) {
      sub.update(isArrayMethod)
    })
  }
}

Dep.target = null // Static property for adding Watcher instances
Copy the code

As you can see from the above code, the implementation of the Dep class is relatively simple, just need to maintain an array management Watcher instance, you can add the Watcher instance, notify the Watcher instance, and call its update method. The notify parameter isArrayMethod is not implemented in the vUE source code. It is only used to identify whether array changes are generated by array methods, and then respond to them.

Now that the implementation of the Dep class is clear, let’s go back to the instantiation of the Observer class and first look at how to implement object listening.

Object listening

In the definition of the Observer class, listening on an object is to call defineReactive on each attribute of the object, which is a key part of the reactive approach, as shown in the following code.

@param {Object} data * @param {string} Key attribute name * @param {*} val value */
function defineReactive(obj, key, value) {
  let val = value
  let childOb = observe(val) If the value is not an object or array, childOb is undefined
  let dep = childOb ? childOb.dep : new Dep()// If the attribute is an object or an array, it is managed by the attribute dep of its Observer instance; otherwise, it is instantiated by a deP instance, using closures to manage the original value
  Object.defineProperty(obj, key, {
    enumerable: true./ / can be enumerated
    configurable: true.// The property descriptor can be changed and deleted from the corresponding object only if and if the property is configured differently. The default is false.
    get: function () {
      / / val closures
      if (Dep.target) { // Add an instance of watcher
        dep.addSub(Dep.target)
      }
      return val
    },
    set: function (newVal) {
      // If the object's attribute is an object or an array, then because it is a reference type, after the value changes, we need to inherit the 'Watcher' instance of the original responsive data deP management, and then listen deeply
      if (childOb) {
        let temp = dep
        childOb = observe(newVal) // Recursive depth traversal to achieve depth monitoringchildOb.dep.subs.push(... temp.subs) dep = childOb.dep }// Val is always in the closure, and after this is set, the latest value will be retrieved when we get it
      val = newVal
      dep.notify()
    }
  })
}
Copy the code

In the above code, there are three special places:

  1. let dep = childOb ? childOb.dep : new Dep()Why is the property an object or an arrayObserverInstance propertiesdepTo manage? In the process of listening on an array, you need this property to implement listening on array methods
  2. getIn the function, with respect todepmanagementWatcherExample that part of the codeDep.targetWhat is?Dep.targetWhat’s stored here isWatcherInstance, because only one can exist at a timeWatcherInstances are managed. It may not be clear, but it will be explained later in the dependency collection
  3. setRecursive traversal of new values in functions is problematic.
    • I’m explaining in the code comment that it’s not exactly right if the property of the object isObjectorArrayIf the new value is of the same type and the dependent attributes inside the value are the same, this is fine.
    • If the property corresponding to the object was the original value, the new value isObjectorArray, then the new value cannot be converted to the response
    • If the corresponding property of the object isObjectorArray, and the new value is the original value, this code will report an error.
    • There is a line of code that aims to bring the original responsive datadepThe management ofWatcherInstance inherited, but I didn’t take into account the originalWatcherInstances where this may result if not neededdepthesubsThe array gets bigger and bigger, and then it runs out of memory.
    • But it’s enough to explain the response, so this code is not optimized.
// If the object's attribute is an object or an array, then because it is a reference type, after the value changes, we need to inherit the 'Watcher' instance of the original responsive data deP management, and then listen deeply
if (childOb) {
  let temp = dep
  childOb = observe(newVal) // Recursive depth traversal to achieve depth monitoringchildOb.dep.subs.push(... temp.subs)// Inherit the original responsive data deP managed 'watcher' instance
  dep = childOb.dep
}
Copy the code

Array listening

In the Observer class definition, Array listening is implemented like this:

if ('__proto__' in {}) {
  data.__proto__ = arrProto
} else {
  addSelfMethod(data, arrProto, arrayKeys)
}
this.observeArr(data)
Copy the code

In this code, observeArr doesn’t have to be explained. The emphasis is on the implementation of data.__proto__ = arrProto and addSelfMethod. Before explaining both of these, it is important to understand that array listening refers to listening for changes caused by some common array methods. Changes of the type arr[0]=4 and arr.length=1 cannot be listened for.

Here we take advantage of the principle of prototype chain. Let’s take a look at the implementation of arrProto and arrayKeys.

// Redefine the array prototype
const oldArrayProperty = Array.prototype
// Create a new object, the prototype points to oldArrayProperty, and extending the new method does not affect the prototype
const arrProto = Object.create(oldArrayProperty) ; ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'].forEach(
  (methodName) = > {
    arrProto[methodName] = function (. args) {
      const result = oldArrayProperty[methodName].call(this. args)// Execute the original array method
      const ob = this.__ob__ // Get the corresponding 'Observer' instance
      let inserted
      switch (methodName) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break
      }
      if (inserted) ob.observeArr(inserted) // Listen for new values
      ob.dep.notify(true) // Notification of change
      return result
    }
  }
)
export default arrProto

const arrayKeys = Object.getOwnPropertyNames(arrProto)
Copy the code

__proto__ is not a standard attribute, so some browsers may not implement it. If it is, then let __proto__ of the original array point to the object we have changed. This will allow us to listen on array methods. If it does not exist, addSelfMethod is called to add the corresponding non-enumerable method to the object to implement the interception.

/** * * @param {obj} target * @param {*} src * @param {*} keys */
function addSelfMethod(target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])// Add the corresponding method to the array object itself}}Copy the code

It is already clear why a DEP instance exists on an Observer instance, and why objects or arrays should be managed by a DEP instance on an Observer instance. This is because it can be accessed and called inside non-definereActive functions.

At this point, with the exception of Watcher, the main responsive core is complete. So, let’s put together a simple reactive class.

The basis ofMVueThe realization of the class

As a framework, there must be an entry point, which is a reactive instance, which is responsible for converting data to reactive, and some lifecycle hooks, which are relatively easy to implement.

import observe from './observer/index'
import { proxy } from './utils/index'

export default class MVue {
  constructor(options) {
    const { el, data, methods, created } = options
    if (data) {
      this.data = typeof data === 'function' ? data() : data // The function gets the return value
      proxy(this.data, this)
      observe(this.data) // Convert the response
    }
    Object.assign(this, methods)
    if (el) {
      this.elSelector = el
    }
    created && created.call(this)}}Copy the code

This implements a simple reactive class that calls observe to implement reactive data, where the proxy function accesses the instance. A is equivalent to.data.a, with the following code:

export function proxy(data, mVue) {
  const me = mVue
  Object.keys(data).forEach(function (key) {
    Object.defineProperty(me, key, {
      configurable: false.enumerable: true.get: function () {
        return me.data[key]
      },
      set: function (newVal) {
        me.data[key] = newVal
      }
    })
  })
}
Copy the code

Depend on the collection

Now that you have responsive instances, and data changes can be monitored, how do you do that? This refers to dependency collection. For dependency collection lessons refer to this article. As I understand it, dependent collection is the reactive data used in the template to perform rendering based on changes in the data. That is, using the properties of a reactive object, add a Watcher instance to the corresponding DEP instance.

So let’s go back to the get function in the defineReactive function that the object listens on.

get: function () {
      / / val closures
  if (Dep.target) { // Add an instance of watcher
    dep.addSub(Dep.target)
  }
    return val
}
Copy the code

When we access the response data, we go through the get function. If we need to collect dependencies, then the dep. target value is the Watcher instance to be managed. Dep.target = null if no collection is required.

So let’s tidy up the idea of relying on collection: Suppose we need the value of attribute A of reactive data in our template. During template compilation, we instantiate a Watcher instance and define the related operations. Then we point dep. target to the Watcher instance to access this attribute in the reactive instance. Target = null and wait for the next collection.

So how do you access the data of a reactive instance?

@param {String} expPath variable path */
export function parsePath(expPath) {
  let path = expPath
  // Implement access to arrays similar to arr[0]
  if (path.indexOf('[')) {
    path = path.replace(/\[/g.'. ')
    path = path.replace(/\]/g.'. ')
    if (/ \. $/.test(path)) {
      path = path.slice(0, path.length - 1)}if (/ \ \. /.test(path)) {
      path = path.replace('.. '.'. ')}}const bailRE = /[^\w.$]/
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('. ')
  return function (object) {
    let obj = object
    for (let i = 0; i < segments.length; i++) {
      if (typeof obj === 'undefined') return ' '
      let exp = segments[i]
      obj = obj[exp]
    }
    if (typeof obj === 'undefined') return ' '
    return obj
  }
}
Copy the code

The above function is to achieve access to responsive data. Passing in the corresponding expression (such as A.B.C) will obtain the corresponding function. Passing in the corresponding responsive instance, the function can obtain the value of the variable by executing the function, that is, calling the GET function.

WatcherThe realization of the class

All that’s left is an executor responsible for executing the action, Watcher, and here’s the code.

import { parsePath } from '.. /utils/index'
import Dep from '.. /observer/dep.js'

export default class Watcher {
  constructor(mVue, exp, callback) {
    this.callback = callback // The callback function
    this.mVue = mVue // Reactive instances
    this.exp = exp / / expression
    Dep.target = this // Start relying on collections
    this.value = this.get() / / call the get
    Dep.target = null // Add
    this.update() // This is the first execution
  }
  async update(isArrayMethod = false) {
    const value = this.get()
    if (this.value ! == value || isArrayMethod) {this.callback(value, this.value) // Call the callback
      this.value = value
    }
  }
  get() {
    const getter = parsePath(this.exp)
    return getter.call(this.mVue, this.mVue)
  }
}
Copy the code

As you can see from the above code, dependency collection takes place at instantiation time. We should just define callbacks at template compilation time.

conclusion

And if I write that, I’m pretty much done with the response. And finally, the whole idea.


  1. MVVM mode introduction ↩︎

  2. Vue. Js and MVVM ↩ ︎