[Vue source code learning] dependency collection

Previously, we learned the responsive principle of Vue, and we know that the underlying layer of vue2 is through object Defineproperty to realize data responsive, but this alone is not enough. The data defined in data may not be used for template rendering. Modifying these data will also start setter and lead to re rendering. Therefore, Vue has optimized here to determine which data changes need to trigger view update by collecting dependencies.

preface

If this article helps you, ❤️ Follow + like ❤️ Encourage the author, the official account and the first time to get the latest articles.

Let's consider two questions first:

  • 1. How do we know where to use the data in data?
  • 2. How to notify render to update the view when the data changes?

In the process of view rendering, the data used needs to be recorded, and the view update is triggered only for the change of these data

This requires dependency collection. You need to create dep for attributes to collect render watcher s

Let's take a look at the official introduction figure. Here, collect as Dependency is the dep.depend() dependency collection in the source code, and Notify is the dep.notify() notification in the source code

Various classes in dependency collection

There are three classes responsible for dependency collection in Vue source code:

  • Observer: observable class, which converts arrays / objects into observable data. Each observer instance member has an instance of Dep (this class was implemented in the previous article)

  • Dep: observation target class. Every data will have an instance of DEP class. There is a subs queue inside it. Subs means subscribers, which saves the observers who depend on the data. When the data changes, call dep.notify() to notify the observers

  • Watcher: Observer class, which is used to wrap observer functions. For example, the render() function will be wrapped into a watcher instance

Dependency is the Watcher. Only the getter triggered by the Watcher can collect dependency. Whichever Watcher triggers the getter will collect which Watcher into Dep. Dep uses the publish subscribe mode. When the data changes, it will cycle the dependency list and notify all watchers. Here I draw a clearer picture:

Observer class

We implemented this class in the last issue. In this issue, we mainly added defineReactive to hijack data g ē The dependency collection is performed when the data setter is hijacked, and the dependency update is notified when the data setter is hijacked. Here is the entry for Vue to collect dependencies

class Observer {
     constructor(v){
         // Each Observer instance has a Dep instance
         this.dep = new Dep()
        // If there are too many data levels, you need to recursively parse the attributes in the object, and add set and get methods in turn
        def(v,'__ob__',this)  //Attach data__ ob__ Attribute, indicating that it has been observed
        if(Array.isArray(v)) {
            // Re hang the rewritten array method on the array prototype
            v.__proto__ = arrayMethods
            // If an object is placed in the array, monitor it again
            this.observerArray(v)
        }else{
            // If you are not an array, you can directly call defineReactive to define the data as a responsive object
            this.walk(v)
        }
        
     }
     observerArray(value) {
         for(let i=0; i<value.length;i++) {
             observe(value[i])
         }
     }
     walk(data) {
         let keys = Object.keys(data); //Get object key
         keys.forEach(key => {
            defineReactive(data,key,data[key]) // Define responsive objects
         })
     }
 }

 function  defineReactive(data,key,value){
     const dep = new Dep() //Instantiate dep, which is used to collect dependencies and notify subscribers of updates
     observe(value) // Recursive implementation of in-depth monitoring, pay attention to performance
     Object.defineProperty(data,key,{
         configurable:true,
         enumerable:true,
         get(){
             //Get value
             // If we are in the stage of relying on mobile phones
             if(Dep.target) {
                 dep.depend()
             }
            //  Dependency collection
            return value
         },
         set(newV) {
             //Set value
            if(newV === value) return
            observe(newV) //Continue to hijack newV. The new value that the user may set is still an object
            value = newV
            console.log('The value has changed:',value)
            // Publish subscribe mode, notification
            dep.notify()
            // cb() / / the subscriber receives a message callback
         }
     })
 }

Hang the instance of Observer class on the__ ob__ On the property, it is used for later data observation, instantiate the Dep class instance, and save the object / array as the value property - if value is an object, execute the walk() process, traverse the object, and turn each item of data into observable data (call the defineReactive method for processing) - if value is an array, execute the observaarray() process, Recursively call observe() on array elements.

Dep class (subscriber)

The role of Dep class is a subscriber. Its main function is to store Watcher observer objects. Each data has an instance of Dep class. There will be multiple observers in a project. However, because JavaScript is single threaded, only one observer can execute at the same time, The Watcher instance corresponding to the observer being executed at the moment will be assigned to the variable Dep.target, so as long as you access Dep.target, you can know who the current observer is.

var uid = 0
export default class Dep {
    constructor() {
        this.id = uid++
        this.subs = [] // subscribes subscribers, storing subscribers. Here is an instance of Watcher
    }

    //Collect observers
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // Add dependency
    depend() {
        // The global location specified by yourself is globally unique
      //The global location specified by yourself is globally unique. When instantiating Watcher, Dep.target = Watcher instance will be assigned
        if(Dep.target) {
            this.addSub(Dep.target)
        }
    }
    //Notify the observer to update
    notify() {
        console.log('Notify observers of updates~')
        const subs = this.subs.slice() // Make a copy
        subs.forEach(w=>w.update())
    }
}

Dep is actually the management of Watcher. It is meaningless for dep to exist alone from Watcher.

  • Dep is a publisher that can subs cribe to multiple observers. After dependency collection, a sub in dep will store one or more observers and notify all watcher s when data changes.
  • The relationship between Dep and Observer is that Observer listens to the whole data, traverses each attribute of data, binds defineReactive method to each attribute, hijacks getter and setter, inserts dependency (dep.depend) into Dep class when getter, and notifies all watcher s to update(dep.notify) when setter.

Watcher class (observer)

Watcher class plays the role of observer. It is concerned with data. After data changes, it is notified and updated through callback function.

According to the above Dep, Watcher needs to realize the following two functions:

  • Add yourself to the sub when dep.depend()
  • Call watcher. When dep.notify() Update() to update the view

At the same time, it should be noted that there are three kinds of watchers: render watcher, computed watcher and user watcher (that is, the watch in vue method)

var uid = 0
import {parsePath} from "../util/index"
import Dep from "./dep"
export default class Watcher{
    constructor(vm,expr,cb,options){
        this.vm = vm // Component instance
        this.expr = expr // Expression to observe
        this.cb = cb // Callback function when the observed expression changes
        this.id = uid++ // Unique identifier of the observer instance object
        this.options = options // Observer options
        this.getter = parsePath(expr)
        this.value = this.get()
    }

    get(){
        // Depending on the collection, set the global Dep.target to Watcher itself
        Dep.target = this
        const obj = this.vm
        let val
        // Keep looking as long as you can
        try{
            val = this.getter(obj)
        } finally{
            // After dependency collection, set Dep.target to null to prevent adding dependencies repeatedly later.
            Dep.target = null
        }
        return val
        
    }
    // When the dependency changes, the update is triggered
    update() {
        this.run()
    }
    run() {
        this.getAndInvoke(this.cb)
    }
    getAndInvoke(cb) {
        let val = this.get()

        if(val !== this.value || typeof val == 'object') {
            const oldVal = this.value
            this.value = val
            cb.call(this.target,val, oldVal)
        }
    }
}

It should be noted that there is a sync attribute in the watcher. In most cases, the watcher is not updated synchronously, but is updated asynchronously, that is, call queueWatcher(this) to push it to the observer queue and call it when nextTick.

The parsePath function here is interesting. It is a high-order function used to parse expressions into getter s, that is, values. We can try to write:

export function parsePath (str) {
   const segments = str.split('.') // First, replace the expression with Cut into one piece of data
  // It returns a function
  	return obj = > {
      for(let i=0; i< segments.length; i++) {
        if(!obj) return
        // Traverse the expression to get the final value
        obj = obj[segments[i]]
      }
      return obj
    }
}

Relationship between Dep and Watcher

Dep is instantiated in the watcher and subscribers are added to dep.subs. Dep traverses dep.subs through notify and notifies each watcher of updates.

summary

Dependency collection

  1. initState: when initializing the computed attribute, the computed watcher dependency collection is triggered
  2. In initState, the user watcher dependency collection is triggered when the listening attribute is initialized (here is the watch we often write)
  3. When render(), the render watcher dependency collection is triggered
  4. When re render, render() executes again, which will remove the subscription of watcer in all subs and re assign the value.
observe->walk->defineReactive->get->dep.depend()->
watcher.addDep(new Dep()) -> 
watcher.newDeps.push(dep) -> 
dep.addSub(new Watcher()) -> 
dep.subs.push(watcher)

Distribute updates

  1. The data of the response is modified in the component to trigger the logic of the setter in defineReactive
  2. Then call dep.notify().
  3. Finally, traverse all subs (watcher instances) and call the update method of each watcher.
set -> 
dep.notify() -> 
subs[i].update() -> 
watcher.run() || queueWatcher(this) -> 
watcher.get() || watcher.cb -> 
watcher.getter() -> 
vm._update() -> 
vm.__patch__()

Recommended reading

Original starting address Click here Welcome to the official account of "front end South nine".

I'm Nan Jiu. I'll see you next time!!!

Keywords: Vue

Added by jossejf on Sat, 29 Jan 2022 05:10:14 +0200