First, the Object. DefineProperty

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

Why is reactive

DefineProperty attributes, get and set

  • get

Property that is called when the property is accessed. No arguments are passed, but this object is passed (because of inheritance, this is not necessarily the object that defines the property). The return value of this function is used as the value of the property

  • set

Property, which is called when the property value is modified. This method takes an argument (that is, the new value being assigned) and passes in the this object at the time of assignment. The default value is undefined

The following is shown in code:

Define a reactive function defineReactive

function update() {
    app.innerText = obj.foo
}

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if(newVal ! == val) { val = newVal update() } } }) }Copy the code

When defineReactive is called, data changes trigger the update method to achieve data responsiveness

const obj = {}
defineReactive(obj, 'foo'.' ')
setTimeout(() = >{
    obj.foo = new Date().toLocaleTimeString()
},1000)
Copy the code

If an object has multiple keys, traversal is required

function observe(obj) {
    if (typeofobj ! = ='object' || obj == null) {
        return
    }
    Object.keys(obj).forEach(key= > {
        defineReactive(obj, key, obj[key])
    })
}
Copy the code

If there are nested objects, recursion in defineReactive is also required

function defineReactive(obj, key, val) {
    observe(val)
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if(newVal ! == val) { val = newVal update() } } }) }Copy the code

When you assign a key to an object, you also need to recurse in the set property

set(newVal) {
    if(newVal ! == val) { observe(newVal)// The new value is the case of the object
        notifyUpdate()
    }
}
Copy the code

The above example can achieve basic responsiveness to an object, but there are still many problems

Deleting and adding attributes to an object cannot be hijacked

const obj = {
    foo: "foo".bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok
Copy the code

It doesn’t work so well when we’re listening on an array

const arrData = [1.2.3.4.5];
arrData.forEach((val,index) = >{
    defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop()  // no ok
arrDate[0] = 99 // ok
Copy the code

You can see that the API for the data can’t be hijacked to make the data responsive,

So in Vue2, the set and DELETE apis were added, and the array API methods were rewritten

Another problem is that if there are deep nested object relationships, the need for deep listening, resulting in a huge performance problem

summary

  • The addition and deletion of object attributes cannot be detected
  • An array ofAPIMethod cannot be listened on
  • Each attribute needs to be traversed and listened on, and if objects are nested, deep listening is required, causing performance problems

Second, the proxy

The listening of the Proxy is for an object, so all operations on that object go into the listening operation, which can completely Proxy all properties

We covered Proxy use in detail in the ES6 series, but we won’t cover it

The following is shown in code:

Define a reactive method

function reactive(obj) {
    if (typeofobj ! = ='object'&& obj ! =null) {
        return obj
    }
    // Proxy adds interception to an object
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(` access${key}:${res}`)
            return res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(` set${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(` delete${key}:${res}`)
            return res
        }
    })
    return observed
}
Copy the code

Test the manipulation of simple data and find that it can be hijacked

const state = reactive({
    foo: 'foo'
})
/ / 1. Access
state.foo // ok
// 2. Set existing properties
state.foo = 'fooooooo' // ok
// 3. Set nonexistent properties
state.dong = 'dong' // ok
// 4. Delete attributes
delete state.dong // ok
Copy the code

When you test the nested objects, it’s not so OK

const state = reactive({
    bar: { a: 1}})// Set nested object properties
state.bar.a = 10 // no ok
Copy the code

To resolve this, you need another layer of proxy on top of GET

function reactive(obj) {
    if (typeofobj ! = ='object'&& obj ! =null) {
        return obj
    }
    // Proxy adds interception to an object
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(` access${key}:${res}`)
            return isObject(res) ? reactive(res) : res
        },
    return observed
}
Copy the code

Third, summary

Object.defineproperty can only be hijacked by traversing Object attributes

function observe(obj) {
    if (typeofobj ! = ='object' || obj == null) {
        return
    }
    Object.keys(obj).forEach(key= > {
        defineReactive(obj, key, obj[key])
    })
}
Copy the code

A Proxy can simply hijack an entire object and return a new object, so we can just manipulate the new object to be responsive

function reactive(obj) {
    if (typeofobj ! = ='object'&& obj ! =null) {
        return obj
    }
    // Proxy adds interception to an object
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(` access${key}:${res}`)
            return res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(` set${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(` delete${key}:${res}`)
            return res
        }
    })
    return observed
}
Copy the code

Proxies can listen for array changes directly (push, Shift, splice)

const obj = [1.2.3]
const proxtObj = reactive(obj)
obj.psuh(4) // ok
Copy the code

Proxy has up to 13 intercepting methods, not limited to Apply, ownKeys, deleteProperty, has, and so on, which Object. DefineProperty does not have

Because of the defects of defineProperty itself, Vue2 needs to implement other methods (such as rewriting array methods, adding extra set and delete methods) in the realization of reactive process.

// Array overwrite
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push'.'pop'.'shift'.'unshift'.'splice'.'reverse'.'sort'].forEach(method= > {
  arrayProto[method] = function () {
    originalProto[method].apply(this.arguments)
    dep.notice()
  }
});

/ / set, delete
Vue.set(obj,'bar'.'newbar')
Vue.delete(obj),'bar')
Copy the code

Proxy is not compatible with IE, there is no polyfill, defineProperty supports IE9

  • Reactive source code