Learn the principles of Vue responsiveness

This article mainly wants to record some problems I have encountered in learning the responsive principle in vue. If there are mistakes, please correct them!

1. What are watcher, Dep and Observer respectively responsible for?

  • Observer is responsible for recursive data and all sub data through object Defineproperty defines getters / setters for properties, listens for changes in data, and defines data as a response. But just put object Defineproperty is encapsulated to monitor data changes. It has no effect. Its main purpose is to collect dependencies.

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter() {
          var value = getter ? getter.call(obj) : val;
          /* ellipsis */
          if (Dep.target) { // Collection dependency
              dep.depend();
          }
          return value
      },
      set: function reactiveSetter(newVal) {
          /* ellipsis */
          dep.notify(); // Trigger dependency
      }
    });
    
    // Generally speaking, collect dependencies in 'getter' and trigger dependencies in 'setter'.
  • The reason for collecting dependencies is to notify where the data is used when the data attributes change.
  • Where is the dependency collection? Dependencies are collected in Dep, which is used to collect dependencies, delete dependencies and notify dependencies.
  • So who is dependence? Dependence is Watcher.

Why it is necessary to encapsulate dependencies into Watcher classes? vue.js gives a good explanation:

When the attribute changes, we need to notify the places where the data is used. There are many places where the data is used, and the types are different. It may be either a template or a watch written by the user. At this time, we need to abstract a class that can deal with these situations centrally. Then, in the dependency collection phase, we only collect the instances of the encapsulated class, and only notify it of one. Then it is responsible for notifying other places.

2. The relationship between dep and Watcher?

When I first started learning Vue source code, I was also confused here:

  1. What does Dep.target do?

Dep.target is actually the current Watcher instance. It is a globally unique static variable. When reading data, the getter of the data will be triggered, and the getter will be executed when triggered

if (Dep.target) {
    dep.depend();
}

Collect yourself into the Dep of the data, and the approximate code is as follows:

 get () {
    /*Set the self watcher observer instance to Dep.target to rely on the collection.*/
    pushTarget(this)
    let value
    const vm = this.vm

    value = this.getter.call(vm, vm)
    
    /*Take the observer instance from the target stack and leave Dep.target empty*/
    popTarget()
    return value
  }

  /*Set the watcher observer instance to Dep.target to rely on the collection. At the same time, the instance is stored in the target stack*/
  function pushTarget (_target: Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
  }
  1. Why does dep collect dependencies by using the addDep method in Watcher, and there is a deps array recording DEP in Watcher?

To answer this question, let's take an example 🌰, Let's take a look at VM in Vue$ watch,

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ): Function {
    const watcher = new Watcher(vm, expOrFn, cb, options)
    /*ellipsis*/
    return function unwatchFn () {
      /*Remove itself from the list of all dependent collection subscriptions*/
      watcher.teardown()
    }
  }
}

/* Watcher/teardown The approximate code is as follows */
/* Remove itself from the list of all dependent collection subscriptions */
  function teardown () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false
  }

This method returns an unwatchFn method to cancel the observation of data. The essence is to remove the Watcher instance from the dependency list of the currently observed data.
If you only record the Watcher in the Dep (dependency list) of the data through the dependency collection in the getter, you only know which watchers are listening to the data in the Dep, but you don't know who you subscribe to (which DEPs the Watcher instance is collected into), and you don't know who you subscribe to, who should be notified to cancel the observation? So we need to record each other in Dep and Watcher and form a many to many relationship.

So we collect dependencies in Dep by using Dep.target addDep(this)

function addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

By recording newDepIds and newDeps, you can not only record which DEPs you have collected in Watcher, but also record yourself in Dep through dep.addSub(this). Isn't it clever!

3. __ob__ What is the function? What does the dep on the Observer instance do?

First__ ob__ The Observer instance is saved on the attribute__ ob__ The first function of is to mark whether the current value has been converted into responsive data by the Observer to avoid subsequent repeated operations.

Secondly, Vue's change detection of Obejct, collection dependency and trigger dependency can access the dep instance in one closure.

function defineReactive () {
  /*Define a dep object in the closure*/
  const dep = new Dep()

  Object.defineProperty(obj, key, {
    get: function reactiveGetter () {
        /*Conduct dependency collection*/
        dep.depend()
    },
    set: function reactiveSetter (newVal) {
      /*dep Object notifies all observers*/
      dep.notify()
    }
  })
}

However, for Array, dependencies are collected in getter s and triggered on interceptors. For unfamiliar partners, please see Vue's code for processing arrays. The general code is as follows:

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    /*The newly inserted element of the array needs to be observe d again to respond*/
    const ob = this.__ob__
    
    /*dep Notify all registered observers for responsive processing*/
    ob.dep.notify()
    return result
  })
})

We can see that in the interceptor, there is no dep instance for us to access, but because the interceptor is a kind of encapsulation of the prototype, we can access the currently operating array (this) in the interceptor, so we should__ ob__ Property is set to each detected data. Here you can use this__ ob__. Dep gets the dep instance.

Finally, I want to say that on the one hand, learning the source code is to more skillfully use the Vue framework to develop, but it is more important to understand why the framework is designed in this way and apply it to future daily development to better improve yourself!

ending

I'm Zhou Xiaoyang, a front-end Mengxin. I wrote this article to record the problems encountered in my daily work and the contents of my study, so as to improve myself. If you think this article is useful to you, please praise and encourage me~

Keywords: Front-end Vue.js

Added by jumpenjuhosaphat on Wed, 16 Feb 2022 16:11:55 +0200