The response principle based on “dependency collection” is explained in depth

clipboard.png

When asked about VueJS’s response principle, everyone may blurt out “Vue Passes.”Object.definePropertyMethodsdataAll properties of the object are converted to getter/setter, and changes are notified when the properties are accessed or modified “. However, many people may not fully understand its deep-seated response principle, and the quality of articles on the network about its response principle is also uneven, mostly with a code and comments. This article will start from a very simple example, step by step analysis of the specific implementation ideas of the response principle.

First, make the data object “observable”

First, we define a data object, taking one of the heroes in the glory of the king as an example:

const hero = {
 health: 3000,
 IQ: 150
 }

We defined the hero’s life value as 3000 and IQ as 150. However, we still don’t know who he is, but it doesn’t matter. We just need to know that this hero will run through our whole article, and our aim is to know who this hero is through his attributes.

Now we can passhero.healthAndhero.IQRead and write directly the attribute values corresponding to this hero. However, when the hero’s attributes are read or modified, we do not know. So what should be done to let the hero voluntarily tell us that his attributes have been modified? At this time need to useObject.definePropertyThe power of the.

AboutObject.definePropertyThe introduction, MDN is said like this:

Object.defineProperty()The method defines a new attribute directly on an object or modifies an existing attribute of an object and returns the object.

In this article, we only use this method to make the object “observable”. For more details about this method, please refer tohttps://developer.mozilla.org …, will not repeat.

So how can this hero inform us of the reading and writing of his attributes? First rewrite the above example:

let hero = {}
 let val = 3000
 Object.defineProperty(hero, 'health', {
 get () {
 Log ('my health attribute was read!'  )
 return val
 },
 set (newVal) {
 Log ('my health attribute has been modified!'  )
 val = newVal
 }
 })

We passedObject.definePropertyMethod, defines a health attribute for hero, which triggers a segment when read or writtenconsole.log. Now let’s give it a try:

console.log(hero.health)
 
 // -> 3000
 //-> my health attribute has been read!
 
 hero.health = 5000
 //-> my health attribute has been modified

It can be seen that the hero has been able to actively tell us the reading and writing of his attributes, which also means that the hero’s data object is already “observable”. In order to make all the hero’s attributes observable, we can think of a way:

/**
 * convert an object into an observable object
 * @param {Object} obj object
 * @param {String} key for key object
 * @param {Any} val object
 */
 function defineReactive (obj, key, val) {
 Object.defineProperty(obj, key, {
 get () {
 //Trigger getter
 Log (`my ${key} attribute read!  `)
 return val
 },
 set (newVal) {
 //Trigger setter
 Log (`my ${key} attribute has been modified!  `)
 val = newVal
 }
 })
 }
 
 /**
 * convert each item of an object into an observable object
 * @param {Object} obj object
 */
 function observable (obj) {
 const keys = Object.keys(obj)
 keys.forEach((key) => {
 defineReactive(obj, key, obj[key])
 })
 return obj
 }

Now we can define heroes as follows:

const hero = observable({
 health: 3000,
 IQ: 150
 })

Readers can try to read and write the hero’s attribute on the console to see if it has become observable.

Second, the calculation properties

Now, the hero has become observable. He will voluntarily tell us any reading and writing operations, but that’s all. We still don’t know who he is. If we hope that after modifying the hero’s life value and IQ, he can take the initiative to tell him other information, how can this be done? It is assumed that this can be done:

watcher(hero, 'type', () => {
 return hero.health > 4000 ?  Tank':' crisp skin'
 })

We have defined awatcherAs a “listener,” it listens to hero’s type attribute. The value of this type attribute depends onhero.healthIn other words, whenhero.healthWhen changes occur,hero.typeChanges should also take place. The former is the dependency of the latter. We can take thishero.typeIt is called “calculation attribute”.

So, how can we correctly construct this listener? As can be seen, in the scenario, the listener receives three parameters, namely the monitored object, the monitored attribute and the callback function, and the callback function returns a value of the monitored attribute. Following this line of thought, we tried to write a piece of code:

/**
 * Called when the value of the calculated attribute is updated
 * @param {Any} val evaluates the value of the attribute
 */
 function onComputedUpdate (val) {
 Log (`my type is: ${val} `);
 }
 
 /**
 :: Observers
 * @param {Object} obj observed object
 * @ param {string} keykey of the observed object
 * @param {Function} cb callback function, which returns the value of "calculation attribute"
 */
 function watcher (obj, key, cb) {
 Object.defineProperty(obj, key, {
 get () {
 const val = cb()
 onComputedUpdate(val)
 return val
 },
 set () {
 Error ('calculation attribute cannot be assigned!'  )
 }
 })
 }

Now we can put the hero in the monitor and try to run the above code:

watcher(hero, 'type', () => {
 return hero.health > 4000 ?  Tank':' crisp skin'
 })
 
 hero.type
 
 hero.health = 5000
 
 hero.type
 
 //-> my health attribute has been read!
 //-> my type is: crispy skin
 //-> my health attribute has been modified!
 //-> my health attribute has been read!
 //-> my type is: tank

There seems to be no problem now. Everything is working well. Is this the end? Don’t forget, we are reading by hand now.hero.typeTo get this hero’s type, he did not voluntarily tell us. If we want the hero to be able to change the health attribute in the first timeActiveWhat should I do if I initiate a notification? This involves the core knowledge of this article-dependent collection.

III. Dependency Collection

We know that when an observable object’s property is read or written, it triggers its getter/setter method. On the other hand, if we can execute in the getter/setter of the observable object, we can execute in the listeneronComputedUpdate()Method, is it able to realize the function of allowing the object to actively send out notifications?

Because of theonComputedUpdate()The method needs to receive the value of the callback function as a parameter, and there is no callback function in the observable object, so we need a third party to help us connect the listener with the observable object.

This third party does one thing-collect the values of callback functions in the listener andonComputedUpdate()Methods.

Now let’s name this third party “dependency collector” and see how it should be written:

const Dep = {
 target: null
 }

It’s as simple as that. Target, which depends on the collector, is used to store inside the listener.onComputedUpdate()Method.

After defining the dependency collector, let’s go back to the listener and see where we should put theonComputedUpdate()The method assigns a value to theDep.target

function watcher (obj, key, cb) {
 //Define a passive trigger function to be called when the dependency of this "observed object" is updated
 const onDepUpdated = () => {
 const val = cb()
 onComputedUpdate(val)
 }
 
 Object.defineProperty(obj, key, {
 get () {
 Dep.target = onDepUpdated
 //Dep.target will be used in the process of executing cb ().
 //reset Dep.target to null when cb () is completed
 const val = cb()
 Dep.target = null
 return val
 },
 set () {
 Error ('calculation attribute cannot be assigned!'  )
 }
 })
 }

We have defined a new within the listeneronDepUpdated()Method, this method is very simple, is the listener callback function value andonComputedUpdate()topackTo a piece, and then assign toDep.target. This step is very critical. Through this operation, the dependency collector obtains the callback value of the listener andonComputedUpdate()Methods. As a global variable,Dep.targetNaturally, it can be used by getter/setter of observable objects.

Take a look at our watcher instance again:

watcher(hero, 'type', () => {
 return hero.health > 4000 ?  Tank':' crisp skin'
 })

In its callback function, called the hero’shealthProperty, which triggers the corresponding getter function. It is important to understand this point clearly, because we need to go back to defining observable objects.defineReactive()Method, rewrite it:

function defineReactive (obj, key, val) {
 const deps = []
 Object.defineProperty(obj, key, {
 get () {
 if (Dep.target && deps.indexOf(Dep.target) === -1) {
 deps.push(Dep.target)
 }
 return val
 },
 set (newVal) {
 val = newVal
 deps.forEach((dep) => {
 dep()
 })
 }
 })
 }

As you can see, we have defined an empty array in this method.depsWhen getter is triggered, it will add one to itDep.target. Return to key knowledge pointsDep.target equals the listener’s onComputedUpdate () method.At this time, the observable object has been bundled with the listener. Whenever the setter of an observable object is triggered, the stored in the array will be calledDep.targetMethod, which automatically triggers theonComputedUpdate()Methods.

As for why heredepsIs an array rather than a variable, because the same attribute may be dependent on multiple calculated attributes, that is, there are multipleDep.target. DefinitiondepsIs an array, if the setter of the current property is triggered, it can call the of multiple calculated properties in batch.onComputedUpdate()Here we go.

After these steps have been completed, basically our entire responsive system has been set up, and the complete code is attached below:

/**
 * Define a "Dependency Collector"
 */
 const Dep = {
 target: null
 }
 
 /**
 * convert an object into an observable object
 * @param {Object} obj object
 * @param {String} key for key object
 * @param {Any} val object
 */
 function defineReactive (obj, key, val) {
 const deps = []
 Object.defineProperty(obj, key, {
 get () {
 Log (`my ${key} attribute read!  `)
 if (Dep.target && deps.indexOf(Dep.target) === -1) {
 deps.push(Dep.target)
 }
 return val
 },
 set (newVal) {
 Log (`my ${key} attribute has been modified!  `)
 val = newVal
 deps.forEach((dep) => {
 dep()
 })
 }
 })
 }
 
 /**
 * convert each item of an object into an observable object
 * @param {Object} obj object
 */
 function observable (obj) {
 const keys = Object.keys(obj)
 for (let i = 0;   i < keys.length;  i++) {
 defineReactive(obj, keys[i], obj[keys[i]])
 }
 return obj
 }
 
 /**
 * Called when the value of the calculated attribute is updated
 * @param {Any} val evaluates the value of the attribute
 */
 function onComputedUpdate (val) {
 Log (`my type is: ${val} `)
 }
 
 /**
 :: Observers
 * @param {Object} obj observed object
 * @ param {string} keythe key of the observed object
 * @param {Function} cb callback function, which returns the value of "calculation attribute"
 */
 function watcher (obj, key, cb) {
 //Define a passive trigger function to be called when the dependency of this "observed object" is updated
 const onDepUpdated = () => {
 const val = cb()
 onComputedUpdate(val)
 }
 
 Object.defineProperty(obj, key, {
 get () {
 Dep.target = onDepUpdated
 //Dep.target will be used in the process of executing cb ().
 //reset Dep.target to null when cb () is completed
 const val = cb()
 Dep.target = null
 return val
 },
 set () {
 Error ('calculation attribute cannot be assigned!'  )
 }
 })
 }
 
 const hero = observable({
 health: 3000,
 IQ: 150
 })
 
 watcher(hero, 'type', () => {
 return hero.health > 4000 ?  Tank':' crisp skin'
 })
 
 Log (`hero initial type: ${hero.type} `)
 
 hero.health = 5000
 
 //-> my health attribute has been read!
 //-> hero initial type: crispy skin
 //-> my health attribute has been modified!
 //-> my health attribute has been read!
 //-> my type is: tank

The above code can be written directly in thecode penOr on the browser console.

Fourth, code optimization

In the above example, the dependency collector is just a simple object, but in factdefineReactive()InternaldepsArrays and other functions related to dependency collection should be integrated inDepExample, so we can rewrite the dependency collector:

class Dep {
 constructor () {
 this.deps = []
 }
 
 depend () {
 if (Dep.target && this.deps.indexOf(Dep.target) === -1) {
 this.deps.push(Dep.target)
 }
 }
 
 notify () {
 this.deps.forEach((dep) => {
 dep()
 })
 }
 }
 
 Dep.target = null

By the same token, we have packaged and optimized observable and watcher to make this responsive system modular:

class Observable {
 constructor (obj) {
 return this.walk(obj)
 }
 
 walk (obj) {
 const keys = Object.keys(obj)
 keys.forEach((key) => {
 this.defineReactive(obj, key, obj[key])
 })
 return obj
 }
 
 defineReactive (obj, key, val) {
 const dep = new Dep()
 Object.defineProperty(obj, key, {
 get () {
 dep.depend()
 return val
 },
 set (newVal) {
 val = newVal
 dep.notify()
 }
 })
 }
 }
class Watcher {
 constructor (obj, key, cb, onComputedUpdate) {
 this.obj = obj
 this.key = key
 this.cb = cb
 this.onComputedUpdate = onComputedUpdate
 return this.defineComputed()
 }
 
 defineComputed () {
 const self = this
 const onDepUpdated = () => {
 const val = self.cb()
 this.onComputedUpdate(val)
 }
 
 Object.defineProperty(self.obj, self.key, {
 get () {
 Dep.target = onDepUpdated
 const val = self.cb()
 Dep.target = null
 return val
 },
 set () {
 Error ('calculation attribute cannot be assigned!'  )
 }
 })
 }
 }

Then let’s run:

const hero = new Observable({
 health: 3000,
 IQ: 150
 })
 
 new Watcher(hero, 'type', () => {
 return hero.health > 4000 ?  Tank':' crisp skin'
 }, (val) => {
 Log (`my type is: ${val} `)
 })
 
 Log (`hero initial type: ${hero.type} `)
 
 hero.health = 5000
 
 //-> hero initial type: crispy skin
 //-> my type is: tank

The code is already in thecode pen, browser console can also be run ~

V. conclusion

Seeing the above code, is it very similar to the VueJS source code? In fact, the thinking and principle of VueJS are similar, but it has done more things, but the core is still here.

When learning VueJS source code, I was confused by the response principle and did not understand it immediately. After continuous thinking and trying, and referring to many other people’s ideas, I finally mastered this piece of knowledge. I hope this article is helpful to all of you. If you find any mistakes or omissions, you are also welcome to point them out to me. Thank you all ~