preface

Everyone has been asked how Vue two-way data binding works, right? It should also be quick to say that Object. DefineProperty makes every property of data a getter/setter, but this is only half the answer, because Object and Array are implemented differently, which is why the title is Object. (It is recommended to see the summary first, and then step by step to see the implementation process)

Basic knowledge of

Let’s start with the following concepts:

Declarative and imperative programming

This concept is popular point said, want to understand in detail can consult information

  • Imperative: telling the computer how to do something, exactly what we tell it to do, no matter what the desired result is.
  • Declarative: We just tell the computer what it wants and let it figure out how to do it. Here’s a simple example to compare the difference between the two,
  // Given an array arr = [1, 2, 3], want a new array with each item incremented by one
  const arr = [1.2.3];
  // The imperative tells the browser to loop through the array, +1 for each element, and then push into the new array
  let newArr1 = [];
  for (let i = 0; i < arr.length; i++) {
    newArr1.push(arr[i]+1);
  }
  console.log(newArr1) // Get the new array

  // The declarative form tells the browser that each item in the new array is the same as each item in the old array
  let result = arr.map(item= > {
    return item + 1;
  })
  console.log(result) // A new array
Copy the code

Why do you say that? Because vue.js is declarative, writing Vue as required by the API documentation knows what to do. (Look back as if off topic, no matter when consolidate knowledge 😂)

Object.defineProperty

Define a new property on an object, or modify an existing property of an object, and return the object. Vue.js uses this method to modify the properties of a data object.

  let name = 'test';
  let obj = {};
  Object.defineProperty(obj, 'name', {
    configurable: true.// Can be modified, can be deleted
    enumerable: true./ / can be enumerated
    get: function() { // Read value trigger
      console.log('Read data');
      return name;
    },
    set: function(newVal) { // Assignment is triggered
      if(name === newVal){
        return;
      }
      console.log('reassign'); name = newVal; }})console.log(obj.name);
  obj.name = 'assignment';
  / / print out
  // Read data
  // test
  // reassign
  / / assignment
Copy the code

Here is already Vue Object two-way data binding principle.

What does Vue do to implement full Object two-way data binding?

Data Monitoring: “Use” and “change”

Object.defineproperty is used to monitor data. When getting a value, get will be triggered for corresponding operations. When setting data, set will be triggered to know whether the data has been changed. Is it clear that we can collect where the data is being used when the data is called to trigger the GET function? Then, at setup time, fire the set function to tell GET to do something about the collected dependencies? Ok, so let’s encapsulate Object.defineProperty for this understanding

defineReactive

function defineReactive(data, key, val) {
  //let dep = [];
  let dep = new Dep() / / modify
  Object.defineProperty(data, key, {
    enumerable: true.configurable: true.get: function() {
      // Collect dependencies
      // dep.push(window.target) // window.target will define 6 operations
      dep.depend() / / modify
      return val
    },
    set: function(newVal){
      if(val === newVal){
        return
      }
      // Trigger dependencies
      // for(let i=0; i<dep.length; i++){
      // dep[i](newVal, val);
      // }
      dep.notify() / / modify
      val = newVal
    }
  })
}
Copy the code

The dependency collection is stored in the DEP array on get, and every dependency in the DEP is triggered when set is triggered. In the source code is the DEP package into a class, to manage dependencies, the following implementation of the DEP class it.

Dep class

export default class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  removeSub(sub) {
    remove(this.subs, sub)
  }
  depend() {
    if(window.target){
      this.addSub(window.target) // what is window.target?
    }
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // window.target update method}}}function remove (arr, item) {
  if(arr.length){
    const index = arr.indexOf(item)
    if(index > - 1) {return arr.splice(index, 1)}}}Copy the code

The Dep class we encapsulated can collect dependencies, delete dependencies, and notify dependencies, so we need to use the Dep class and modify defineReactive above. The dependencies collected by the Dep are known by code as window.target. When data changes, the window.target update method is called in response to the update.

Watcher class

There is a Watcher class in the source code, and its instance is the window.target we collected. Let’s take a look at one of the uses in Vue

vm.$watch('user.name'.function(newVal, oldVal){
  console.log('My new name' + newVal); // The update function
})

Copy the code

When data.user.name is modified in a Vue instance, function will be executed, which means that this function needs to be added to the dependency of data.user.name. The data.user.name get method can be adjusted. So all Watcher has to do is add its own instance to the Dep of the corresponding property, and also have the ability to notify to update, so Watcher

export default class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get() // Get the initial value
  }
  get() {
    window.target = this // By exposing the current instance to the Dep, the Dep knows who the dependency is
    let value = this.getter.call(this.vm, this.vm) // Trigger the get method on the corresponding attribute on the VM instance to collect the dependency
    window.target = undefined // Give it to someone else
    return value
  }
  update() {
    const oldValue = this.value / / the old value
    this.value = this.get() // Get a new value
    this.cb.call(this.vm, this.value, oldValue)
  }
}
Copy the code

If you look back at some of the programs I’ve written above, you’ll see that there are clever combinations, especially with the Watcher example, which adds itself to the Dep, which I think is pretty cool anyway. It also shows that de$watch in Vue is implemented via Watcher.

Watcher knows that parsePath returns a method that returns a value when it is called

const bailRE = /[^\w.$]/
export function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.spilt('. ')
  return function(obj){
    for(let i = 0; i < segments.length; i++){
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

This. Getter. Call (this.vm, this.vm) in Watcher refers parsePath’s return function to this.vm and passes this as an argument.

The Observer class

Every attribute of data in Vue is detected. In fact, we use defineReactive to detect if a data has a lot of attributes that need to be called many times. Observer is a utility class that turns each attribute into a getter/setter for the code

export class Observer {
  constructor(value) {
    this.value = value
    if(!Array.isArray(value)){
      this.walk(value)
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key= > {
      defineReactive(obj, key, obj[key])
    })
  }
}
Copy the code

The new Observer(obj) can change all properties of obj into getters/setters. What if obj[key] is still an object? Define obj[key]; define obj[key]; define obj[key]

function defineReactive(data, key, val) {
  if(typeof val === 'object') {
    new Observer(val)
  }
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true.configurable: true.get: function() {
      dep.depend()
      return val
    },
    set: function(newVal){
      if(val === newVal){
        return
      }
      dep.notify()
      val = newVal
    }
  })
}
Copy the code

Vue provides $set and $delete to implement these two functions. Vue provides $set and $delete to implement these two functions. The implementation of these two is not difficult.

conclusion

For Object’s data response in Vue, MY summary is “define getters/setters for standby,” use “: collect dependencies,” change “: trigger dependencies

  • Standby: PassObserveranddefineReactiveChange the property togetter/setter;
  • Use: passWatcheringetterTo collect dependencies fromdep;
  • “Change” : passsettertelldepThe data has changed,depnoticeWatcherTo update;