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
- initState: when initializing the computed attribute, the computed watcher dependency collection is triggered
- In initState, the user watcher dependency collection is triggered when the listening attribute is initialized (here is the watch we often write)
- When render(), the render watcher dependency collection is triggered
- 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
- The data of the response is modified in the component to trigger the logic of the setter in defineReactive
- Then call dep.notify().
- 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
- [Vue source code learning] exploration of responsive principle
- Causes and solutions of unreliable execution of JS timer
- From how to use to how to implement a Promise
- Super detailed explanation of page loading process
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!!!