Responsive system and implementation

Response principle

The responseful principle of vue.js relies on Object.defineProperty, as mentioned in the vue.js documentation, which is why vue.js does not support IE8 and earlier browsers. Vue listens for changes in data by setting setter/getter methods for object properties, relies on the getter for collection, and each setter method is an observer that notifies the subscriber to update the view when the data changes.

Let data to observable

So let’s assume the simplest case, and let’s not worry about the other cases. Observe is called in initData to set the Vue data to observable. Set is triggered when the _data data changes, performing a callback to the subscriber (render in this case).

function observe(value, cb) {
    Object.keys(value).forEach((key) = > defineReactive(value, key, value[key] , cb))
}

function defineReactive (obj, key, val, cb) {
    Object.defineProperty(obj, key, {
        enumerable: true.configurable: true.get: (a)= >{
            / *... Dependency collection etc.... * /
            /*Github:https://github.com/answershuto*/
            return val
        },
        set:newVal= > {
            val = newVal;
            cb();/* The subscriber receives a callback to the message */}})}Copy the code

For ease of operation, we need to proxy the data on _data to the VM instance.

function proxy (data) {
    const that = this;
    Object.keys(data).forEach(key= > {
        Object.defineProperty(that, key, {
            configurable: true.enumerable: true.get: function proxyGetter () {
                return that._data[key];
            },
            set: function proxySetter (val) { that._data[key] = val; }})}); }Copy the code

Depend on the collection

Reasons for relying on collections

One problem with binding as described above is that unused data in the actual template will be re-rendered if it is changed, which is a performance drain, so you need to rely on collection to ensure that only the data used in the actual template is rendered.

Dep

When the setter is triggered to modify the value of the object on data, the getter event will be triggered naturally when the value is set. Therefore, we only need to render once at the beginning, and all the data in the rendered dependent data will be collected by the getter into the SUBs of the Dep. Setters only fire the subs function of the Dep when modifying the data in the data.

The dep.prototype. depend method assigns the observer Watcher instance to the global dep. target, and then triggers the Render operation to collect dependencies only on objects marked by dep. target. The object with dep. target will push the instance of Watcher into subs. When the setter operation is triggered by the modification of the object, Dep will call the update method of the Watcher instance in subs to retrieve the data and generate the virtual node, and then the server will render the virtual node into the real DOM.

src/oberver/dep.js

var uid = 0;

//dep constructor
export default function Dep(argument) {
	this.id = uid++
	this.subs = []
}
// Add an observer object
Dep.prototype.addSub = function(sub) {
	this.subs.push(sub)
}
// Remove an observer object
Dep.prototype.removeSub = function(sub) {
	remove(this.subs, sub)
}
// Rely on collection
Dep.prototype.depend = function() {
	if(Dep.target) {
		Dep.target.addDep(this)}}// Notify all subscribers
Dep.prototype.notify = function() {
	var subs = this.subs.slice()
	for(var i = 0, l = subs.length; i < l; i++){
		subs[i].update()
	}
}

Dep.target = null

function remove (arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item)
        if (index > - 1) {
            return arr.splice(index, 1)}}Copy the code

implementation

I’ve covered the principle of responsiveness and the reasons for relying on collection, so let’s make it simple. Write test cases as usual before you begin formal programming.

test/observer/observer.spec.js

import {
  Observer,
  observe
} from ".. /.. /src/observer/index"
import Dep from '.. /.. /src/observer/dep'

describe('Observer test'.function() {
  it('observing object prop change'.function() {
  	const obj = { a:1.b: {a:1}, c:NaN}
    observe(obj)
    // mock a watcher!
    const watcher = {
      deps: [],
      addDep (dep) {
        this.deps.push(dep)
        dep.addSub(this)},update: jasmine.createSpy()
    }
    // observing primitive value
    Dep.target = watcher
    obj.a
    Dep.target = null
    expect(watcher.deps.length).toBe(1) // obj.a
  });

});
Copy the code

Now formally implement the data binding, where Observe returns an Observer instance and observer implements the data binding.

src/observer/index.js

import {
  def, //new
  hasOwn,
  isObject
}
from '.. /util/index'

export function Observer(value) {
  this.value = value
  this.dep = new Dep()
  this.walk(value)
  def(value, '__ob__'.this)}export function observe (value){
  if(! isObject(value)) {return
  }
  var ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

Observer.prototype.walk = function(obj) {
  var keys = Object.keys(obj)
  for (var i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
  }
}

export function defineReactive (obj, key, val) {
  var dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      var value = val
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value =  val
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
	   val = newVal
      dep.notify()
    }
  })
}
Copy the code

In the above code we have used some utility functions. Now we will implement these utility functions in a separate file, so that other components can call later.

src/util/index.js

const hasOwnProperty = Object.prototype.hasOwnProperty
// The passed argument must be evaluated, otherwise an error will be reported if obj is null
export function hasOwn(obj, key) {
  if(! isObject(obj) && !Array.isArray(obj)) {
    return
  }
  return hasOwnProperty.call(obj, key)
}

export function isObject(obj) {
	returnobj ! = =null && typeof obj === 'object'
}
// The _ob_ attribute for the object to be observed stores the Observer object, marking the observed object
export function def(obj, key, val, enumerable) {
	Object.defineProperty(obj, key, {
		value: val,
		enumerable:!!!!! enumerable,writable: true.configurable: true})}Copy the code

Now that you’ve done a simple data binding, use the NPM run test command to test your project.