preface

I believe you were not asked about the responsive principle of VUE during the interview. You may all say that the properties in an Object are converted to get and set by publishing and subscribing + data hijacking (object.defineProperty). When the properties are modified or accessed, you will be notified of changes. Most people are probably aware of this aspect and don’t fully understand it. Starting with a simple example, this article will delve step by step into the principles of responsiveness.

Observable objects

For a simple example, let’s define an object:

const hero = {
    hp: 1000.ad: 100
}
Copy the code

Here we define a hero with an HP of 1000 and an AD of 100.

Now we can read and write the corresponding property values through hero.hp and hero.ad, but we don’t know when the hero property is being read or written.

Object. DefineProperty can be used in the corresponding get and set.

let hero = {}
let val = 1000
Object.defineProperty(hero, 'hp', {
    get() {
        console.log('HP attribute read! ')
        return val
    },
    set(newVal) {
        console.log(The 'HP attribute has been modified! ')
        val = newVal 
    }
})
Copy the code

Using the object.defineProperty method, we define an HP property for hero, which triggers a console.log when read or written. Here’s a try:

hero.hp
/ / - > 1000
// -> the HP attribute was read!

hero.hp = 4000 
// -> the HP attribute has been modified.

Copy the code

As you can see, the hero can now actively tell us what reads and writes its properties, which means that the hero’s data object is now observable. In order to make all the attributes of the hero observable, we can think of a way:

/** * convert an object to an observable *@param { Object } Object obj *@param { String } Key * of the key object@param { Any } The value of a key in the val object */
function reactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(` my${key}The property has been read! `)
            return val
        },
        set(newVal) {
            console.log(` my${key}The property has been modified! `)
            val = newVal
        }
    })
}

/** * convert each item of an object into an observable *@param { Object } * / object obj
function observable(obj) {
    const keys = Object.keys(obj)
    keys.forEach((key) = > { reactive(obj, key, obj[key]) })
    return obj
}
Copy the code

Now you can use the above method to define a responsive hero object.

const hero = observable({
    hp: 1000.ad: 100
})
Copy the code

You can try reading and writing the hero attributes on the console to see if it has become observable.

Calculate attribute

Now that the object is observable, he will proactively tell us about any read or write, what if we want him to proactively tell us something else after we’ve changed the value of the object’s property? Let’s say I have a watcher method

watcher(hero, 'type'.() = > {
    return hero.hp <= 1000 ? 'back' : 'the tanks'
})
Copy the code

We define a watcher as a listener that listens for the Type property of the hero. The value of this type attribute depends on hero.hp. In other words, when hero.hp changes, hero.type should also change. The former is dependent on the latter. We can call this hero.type a computed property.

Watcher’s three arguments are the object being listened on, the property being listened on, and the callback function. The callback function returns a value for the property being listened on. Along this line of thinking, let’s try to write a piece of code:

/** * is called when the value of the evaluated property is updated@param { Any } Val evaluates the value of the attribute */ 
function computed(val) { 
    console.log('My type is:${val}`);
}

/** * Observer *@param { Object } Obj Observed object *@param { String } Key Key * of the observed object@param { Function } Cb callback function that returns the value of the evaluated property */
function watcher(obj, key, cb) {
    Object.defineProperty(obj, key, {
        get() {
            const val = cb()
            computed(val)
            return val
        },
        set() {
            console.error('Calculated properties cannot be assigned! ')}}}Copy the code

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

watcher(hero, 'type'.() = > {
    return hero.hp <= 1000 ? 'back' : 'the tanks'
})
hero.type 
hero.hp = 4000 
hero.type
// -> my HP attribute was read!
// -- > < span style = "box-sizing: border-box; color: RGB (74, 74, 74)
// -> my HP properties have been modified!
// -> my HP attribute was read!
// -- > < span style = "box-sizing: border-box; color: RGB (74, 74, 74)

Copy the code

That’s all well and good, but now we’re getting the hero’s type from hero.type, not from hero.type. If you want his HP to change, you can tell us what to do immediately. —- Dependency Collection

Depend on the collection

When an observable object is read, the corresponding GET and set are triggered. If a listener performs computed in this object, can the object issue a notification?

Because the computed method needs to accept a callback function that does not exist in the observable, a “mediator” needs to be established to connect the observable to the listener.

The callback function used by the mediation to collect the value of the listener is a first-level computed() method

This mediation is called a dependency collector:

const Dep = {
    target: null
}
Copy the code

Target is used to store the computed methods in the listener.

Go back to the listener and see where to assign computed to dep.target

/** * Observer *@param { Object } Obj Observed object *@param { String } Key Key * of the observed object@param { Function } Cb callback function that returns the value of the evaluated property */
function watcher(obj, key, cb) {
    // Define a passive trigger function to be called when the observed object's dependency is updated
    const onDepUpdated = () = > { 
        const val = cb() 
        computed(val) 
    }
    
    Object.defineProperty(obj, key, {
        get () { 
            Dep.target = onDepUpdated 
            // dep. target is used when executing cb(),
            // Reset dep. target to null when cb() finishes executing
            const val = cb() 
            Dep.target = null 
            return val 
        }, 
        set () { 
            console.error('Calculated properties cannot be assigned! ')}}}Copy the code

We define a new onDepUpdated() method inside the listener that simply bundles the value of the listener callback with computed() and assigns it to dep.target. This is the key step by which the dependent collector gets the callback value of the listener as well as the computed() method. As a global variable, dep. target is naturally used by getters/setters of observable objects.

Look again at our Watcher example:

watcher(hero, 'type'.() = > {
    return hero.hp <= 1000 ? 'back' : 'the tanks'
})
Copy the code

In its callback function, the hero’s HP property is called, which triggers the corresponding GET function. It’s important to understand this, because next we need to go back to the reactive() method and rewrite it:

/** * convert an object to an observable *@param { Object } Object obj *@param { String } Key * of the key object@param { Any } The value of a key in the val object */
function reactive(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() 
            })
        }
    })
}
Copy the code

As you can see, in this method we define an empty array deps, and when get is fired, we will add a dep.target to it. Back to the key point dep.target equals the listener’s computed() method, at which point the observable is tied to the listener. Any time the set of the observable is fired, the dep.target method stored in the array is called, which automatically triggers the computed() method inside the listener.

The reason why deps is an array and not a variable is that it is possible that the same property is dependent on multiple computed properties, i.e. there are multiple dep.target. Define deps as an array. If the set of the current property is fired, you can call multiple computed() methods for the property in a batch.

After completing these steps, our entire responsive system is basically set up, and the complete code is pasted below:

/** * Define a dependency collector */
const Dep = {
    target: null
}

/** * convert an object to an observable *@param { Object } Object obj *@param { String } Key * of the key object@param { Any } The value of a key in the val object */
function reactive(obj, key, val) {
    const deps = []
    Object.defineProperty(obj, key, {
        get() {
            console.log(` my${key}The property has been read! `)
            if (Dep.target && deps.indexOf(Dep.target) === -1) { 
                deps.push(Dep.target) 
            } 
            return val
        },
        set(newVal) {
            console.log(` my${key}The property has been modified! `)
            val = newVal 
            deps.forEach((dep) = > { 
                dep() 
            })
        }
    })
}

/** * convert each item of an object into an observable *@param { Object } * / object obj
function observable(obj) {
    const keys = Object.keys(obj)
    keys.forEach((key) = > { reactive(obj, key, obj[key]) })
    return obj
}

/** * is called when the value of the evaluated property is updated@param { Any } Val evaluates the value of the attribute */ 
function computed(val) { 
    console.log('My type is:${val}`);
}

/** * Observer *@param { Object } Obj Observed object *@param { String } Key Key * of the observed object@param { Function } Cb callback function that returns the value of the evaluated property */
function watcher(obj, key, cb) {
    // Define a passive trigger function to be called when the observed object's dependency is updated
    const onDepUpdated = () = > { 
        const val = cb() 
        computed(val) 
    }
    
    Object.defineProperty(obj, key, {
        get() { 
            Dep.target = onDepUpdated 
            // dep. target is used when executing cb(),
            // Reset dep. target to null when cb() finishes executing
            const val = cb() 
            Dep.target = null 
            return val 
        }, 
        set() { 
            console.error('Calculated properties cannot be assigned! ')}}}const hero = observable({
    hp: 1000.ad: 100
})

watcher(hero, 'type'.() = > {
    return hero.hp <= 1000 ? 'back' : 'the tanks'
})

console.log('Hero initial type:${hero.type}`)

hero.hp = 4000

// -> my HP attribute was read!
// -> Hero initial type: back row
// -> my HP properties have been modified!
// -> my HP attribute was read!
// -- > < span style = "box-sizing: border-box; color: RGB (74, 74, 74)
Copy the code

The above code can be executed directly from the browser console

Code optimization

In the example above, the dependency collector is a simple object. The deps array inside reactive() should be integrated into the Dep instance, 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
Copy the code

In the same way, we encapsulated and optimized both Observable and Watcher to make the responsive system modular:

class Observable{
    constructor(obj) { 
        return this.walk(obj) 
    }
    walk(obj) { 
        const keys = Object.keys(obj) 
        keys.forEach((key) = > { 
            this.reactive(obj, key, obj[key]) 
        }) 
        return obj 
    }
    reactive(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, computed) { 
        this.obj = obj 
        this.key = key 
        this.cb = cb 
        this.computed = computed 
        return this.defineComputed() 
    }
    
    defineComputed() { 
        const self = this 
        const onDepUpdated = () = > { 
            const val = self.cb() 
            this.computed(val) 
        } 
        Object.defineProperty(self.obj, self.key, { 
            get() { 
                Dep.target = onDepUpdated 
                const val = self.cb() 
                Dep.target = null 
                return val 
            }, 
            set() { 
                console.error('Calculated properties cannot be assigned! ')}})}}Copy the code

Try it out:

const hero = new Observable({
    hp: 1000.ad: 100
})

new Watcher(hero, 'type'.() = > {
    return hero.hp <= 1000 ? 'back' : 'the tanks'
}, (val) = > {
    console.log('My type is:${hero.type}`)})console.log('Hero initial type:${hero.type}`) 

hero.hp = 4000

// -> Hero initial type: back row
// -- > < span style = "box-sizing: border-box; color: RGB (74, 74, 74)
/ / - > 4000
Copy the code

The above code can be executed directly from the browser console

At the end

Is the above code very similar to the source code in VUE? In fact, the idea is the same, this article to pick out the core part for everyone to eat. If you learn vUE source code, do not know how to start, I hope this article can give you help. The author also referred to many other people’s thoughts and continuous attempts to master.

This article is the author’s notes quite a long time ago to rearrange an article for everyone to eat, if you have comments or other questions welcome to point out, if it helps you please remember to like the attention collection triple combo.