As we all know, VUE mainly listens to data through object.defineProperty to encapsulate the Object. In VUe3, it mainly uses proxy. This is often asked in interviews, but many front end students will answer this question with Object. DefineProperty. In fact, this answer is very negative. Let’s take a closer look at how data is listened on in VUe2.

Object.defineProperty

let data = {key: 1}
Object.defineProperty(data, 'key', {
    get: function (val) {
        alert('get')
    },
    set: function (newVal) {
        alert('set')
    }
})
data.key
data.key = 1
Copy the code

Object. DefineProperty is mainly used to detect attribute changes in an Object. Copy the appeal code and type it in the browser. We can see that the browser pops up the get string for data.key and the set string for data.key = 1. Object.defineproperty listens for reads and writes of attributes of objects. With this feature, we can collect dependencies in getters and start dependencies in setters

Rely on

What is dependency? By dependency, we mean where the property is used, or it could be the developer’s own watcher, and we call get whenever we need to call this property. For example,

<p>{{text}}</p>
...
data () {
    return {
        text: 1
    }
}
Copy the code

In the current template, the p tag to display text must call the value of text once, which triggers a get method. This is one place to use this property, collect the current dependency, remember that this is the place to use this property. Put it in an array to manage.

Dependency management

There are so many attribute variables in a project that storing dependencies in arrays every time would be disastrous. Here we introduce a Dep class.

Constructor () {this.subs = []} addSub (sub) {//... } removeSub(sub) { // .. If (dep.target) {dep.target.adddep (this)}} notify () {const subs = This.subs.slice () subs.foreach (sub => sub.update()) // Notify each dependency point of update events}}Copy the code

If and only dep. target is not null, the current operation is to add a subscriber. The subscriber is dep. target. This is known as a dependency (where this variable is used). To sum up, there are three steps to collecting dependencies

  • Put yourself on dep.target

  • The value of the dependent key is specified

  • Remove yourself from dep.target

With the Dep class, this makes managing properties a lot easier. Each time we listen for an object’s properties, we create a new Dep object to manage the current properties. Vue bidirectional binding code is as follows:

function defineReactive (data, key, val) {
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend()
            return val
        }
        set: function () {
            if (val === newVal) {
                return 
            }
            val = newVal
            dep.notify()
        }
    })
}
Copy the code

In this way, we only need to manage the listening properties for Dep objects.

Observer

With Watcher, we implement data change detection. In order to detect all attributes in the data, we wrap an Observer class.

class Observer { constructor (value) { this.value = value if(! Array.isArray(value)) { this.walk(value) } } walk(obj) { const keys = Object.keys(obj) keys.forEach(key => { defineReactive(obj, key, obj[key]) }) } } function defineReactive (data, key, val) { let dep = new Dep() Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.depend() return val } set: function () { if (val === newVal) { return } val = newVal dep.notify() } }) }Copy the code

The above code is the prototype of vuE2’s bidirectional binding. There are also two defects that we can see from this. As you can see from the above code, this method can only listen on existing attributes of the object, but not on new and deleted attributes of vue2 (experienced developers of vue2 should have experienced this problem, and must first initialize a value). Object defineProperty does not allow you to listen on an array. Object defineProperty does not allow you to listen on an array property.

An array of listening

Let’s take a look at some of the native ways that Array provides to change your organization:

push pop shift unshift splice sort reverse

All of the above methods can change the structure of an array. Let’s see why the above logic is not suitable for listening on an array.

function defineReactive (data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val
        },
        set: function (newVal) {
            alert('set')
        }
    })
}

let obj = {
    arr: [1, 2, 3]
}
defineReactive(obj, 'arr', obj.arr)
obj.arr.push(4)
Copy the code

If we print obj. Arr, we find that the array has changed. If we print obj. Arr = 1, we find that the set string pops up again, so we can conclude: When ojbect.defineProperty binds an array, methods like push don’t start our listening event.

How do we trigger event listeners when we change the structure of the array itself using methods like push? Code is dead, people are alive. Let’s introduce interceptors to enhance array listening.

const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((method) => { const original = arrayProto[method] Object.defineProperty(arrayMethods, method, { value: function mutator(... Args) {notify() return original. Apply (this, args)}, Enumerable: false, writable: true, different: 64x true }) }) class Observer { constructor (value) { this.value = value if (Array.isArray(value)) { value.__proto__ = arrayMethods } else { this.walk(value) } } }Copy the code

This is the interceptor creation process. To avoid contaminating the global Array object, we create a new empty object (the interceptor). Add the Array [‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’] methods to empty objects, and add the desired notification events to each function. Finally, the prototype pointer to the target array points to the interceptor. This triggers a listening event when the above function is called.

With the addition of the interceptor above, we have the ability to notify array changes, but how to do so. As mentioned in the previous knowledge point, object properties are collected through the getter, and the getter fires the key value of the bound object, that is, whether it is an array or not, it is collected through the getter. There are differences in the way they manage dependencies, and I won’t go into that here. If you’re interested, you can search for it yourself.