This is the fifth day of my participation in Gwen Challenge

This paper is a study note on the principle of data responsiveness, aiming to better understand the underlying principle of Vue. It is long, so it is divided into several parts, and this is the end part. Portal “Data Responsive Principle – 01” “Data Responsive Principle – 02”

In the previous article, we implemented reactive handling of arrays. In this article, we introduce dependency collection and the Watcher class.

Collect rely on

We’ve already made the data reactive, but how do we make user-defined methods call when the data changes? New Watcher will pass in a callback as a parameter. This callback can be a user-defined method. The callback will be called when the data in the Watcher response changes.

So why does Watcher call callback when its data changes? This involves collecting dependencies. First of all, the data is already observe-that is, it’s already responsive. In the constructor of the Watcher, it gets the value of the data to subscribe to, and that fires the getter for the data, and when it fires the getter collects the Watcher instance into an array subs, and when the data is changed, it fires the setter, Then in the setter it loops through the subs array, notifying one by one, executing the update method, and finally firing the callback from the update method.

What is dependency?

  • Where data is needed is called a dependency. In Ve2. X, the component that uses data is a dependency. Notify the component when data changes and perform diff algorithms within the component via the virtual DOM
  • Dependencies are collected in getters and fired in setters

Dep class

The Dep class encapsulates the dependency collection code and manages dependencies

// Dep.js
export default class Dep {
  constructor(arg) {
     // Store your subscribers in an array containing Watcher instances
     this.subs = []
  }
  
  // Add a subscription
  addSub(sub) {
    this.subs.push(sub)
  }
  
  // Add dependencies
  depend() {
    // dep. target is a globally unique location that we specify, as well as window.target
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  
  // Notification update
  notify() {
    const subs = this.subs.slice() / / shallow clone
    subs.forEach(item= > {
      item.update()
    })
  }
}
Copy the code

Each Observer instance has an instance of Dep

Const dep = new dep () in the constructor function of the Observer class

// Observer.js.import Dep from './Dep.js'
export default class Observer {
  constructor(value) {
    this.dep = new Dep() // In the case of this note, it is not necessary to write here. }... }Copy the code

There is another place where Dep instances are also created, which is in defineReactive

The purpose is to notify dep.notify() when a setter is detected.

// defineReactive.js
import Dep from './Dep.js'
export default function defineReactive(data, key, value) {
  const dep = new Dep()
  ...
  Object.defineProperty(data, key, {
    ...
    set(newValue){...// Trigger dependencies in setters
      dep.notify()
    }
  })
}
Copy the code

Dep uses a publisk-subscribe model, which loops through the list to notify all watchers when data changes. In this way, whenever you modify obj, such as obj.b = 3, the notify method of Dep is executed. Ob.dep.notify () = ob.dep.notify() = ob.dep.notify() = ob.dep.notify()

// array.js . methodsCouldChange.forEach(item= >{... def(arrayMethods, item,function() {... ob.dep.notify() ... },false)})...Copy the code

Watcher class

Say first purpose

Our final goal is to create an instance of the Watcher class in index.js to monitor the specified properties of the specified object, and to get the value of the object properties in the third parameter of new Watcher() callback function, so that we can do something we want to do.

// index.js
import observe from './observe.js'
import Watcher from './Watcher.js'

let obj = {
  a: {
    m: {
      n: 1
    }
  }
}
observe(obj)
new Watcher(obj, 'a.m.n'.(val, oldValue) = > {
  console.log('watcher', val, oldValue)
})
obj.a.m.n = 2
Copy the code

The desired outcome is 1



That is, as long as I create a new Watcher instance and pass in the properties I want to monitor (A.M.N) and object (obj), I can get the old and new properties of obj.a.m.n in Watcher’s third argument, which is a callback function, and continue to do something. Diff algorithm and so on. Let’s start writing the Watcher class:

Create a new watcher.js file

// Watcher.js
import Dep from './Dep.js'
let uid = 0
export default class Watcher {
  constructor(target, expression, callback) {
    this.id = uid++ // Let each watcher instance have its own ID
    this.target = target // target is the object passed in for new instance to monitor (obj)
    this.getter = parsePath(expression) // Getter will be a function called in the get defined below
    this.callback = callback // Callback is the callback function passed in
    this.val = this.get() // Gets the expression property of the target object
  }

  // Data update triggered
  update() {
    this.run()
  }

  get() {
    // The Watcher instance itself, which assigns dep. target to new, enters the dependency collection phase
    Dep.target = this
    const obj = this.target
    
    let value
    try {
      DefineReactive is triggered because obj has been observed. defineProperty get() is triggered */
      value = this.getter(obj)
    } finally { // Executes after the try block, regardless of whether an exception is thrown or caught
      Dep.target = null // Exit dependency collection
    }
    return value
  }

  run() {
    this.getAndInvoke(this.callback)
  }

  getAndInvoke(cb) {
    const newValue = this.get()
    if(newValue ! = =this.val || typeof newValue === 'object') {
      const oldValue = this.val
      cb.call(this.target, newValue, oldValue)
    }
  }
}

// Pass in a property string such as A.M.N and return a getter. If you pass in obj to this function, you can get the value of obj.a.m.n
const parsePath = function(str) {
  const segments = str.split('. ')
  return function(obj) {
    const value = segments.reduce((accumulator, currentValue) = > {
      return accumulator = accumulator[currentValue]
    }, obj)
    return value
  }
}
Copy the code

At this point, let’s pause to clarify our thoughts: To do that, we created a new Watcher class, When we do new Watcher(obj, ‘A.M.N ‘, (val, oldValue) => {console.log(‘ Watcher ‘, val, oldValue)}) in index.js, The constructor of the Watcher class is executed, and it says this.val = this.get(). This is the key statement. In get(), it does two things:

  1. throughDep.target = thisStart collecting dependencies and assign the global variable dep. target to the Watcher instance itself.
  2. throughvalue = this.getter(obj)Obj = obj; obj = obj; obj = obj; obj = obj; obj = obj; obj = obj;

Then we can make the following changes to definereactive.js

// defineReactive.js.export default function defineReactive(data, key, value) {...Object.defineProperty(data, key, {
    ...
    get() {
      // If you are in the dependency collection phase (collecting dependencies in the getter)
      if (Dep.target) {
        dep.depend()
      }
      returnvalue }, ... })}Copy the code

A dependency is a Watcher, and only the getter triggered by the Watcher will collect the dependency. The Watcher that triggered the getter will collect the dependency into the Dep.

At this point, the data response principle of the content of the study notes to share the end, inevitably there are omissions, please correct.