Origin: Vue’s data binding is used to listen for changes in an object’s properties, and to make corresponding view updates through dependency collection, etc.
Question: Can all types of property changes on an object be monitored?
Object. DefineProperty is used to simply listen for Object property changes through the getter/setter of the Object, and to do the corresponding dependency handling through dependencies.
However, this can be problematic, especially if the value of an attribute in an object is an array. As the Vue documentation states:
Due to JavaScript limitations, Vue cannot detect the following array changes:
- When you set an item directly using an index, for example
vm.items[indexOfItem] = newValue
- When you modify the array length, for example
vm.items.length = newLength
Vue source code can also see that it is indeed the array to do special processing. The reason is that ES5 and below do not inherit arrays perfectly.
Experiment?
Observe was used to perform a simple experiment as follows:
import { observe } from './mvvm'
const data = {
name: 'Jiang'.userInfo: {
gender: 0
},
list: []}// The getter/setter is used directly here
observe(data)
data.name = 'Solo'
data.userInfo.gender = 1
data.list.push(1)
console.log(data)
Copy the code
The result is this:
The result shows the problem. The values of name, userInfo, and list in data are all changed. However, the change of list is not detected by Observe. The reason? In short, the method that operates on an Array, that is, the method mounted on array. prototype, does not fire the setter for that property because it does no assignment.
How to solve this problem?
The solution to this problem in Vue is to rewrite the usual array methods so that they can be heard when called.
Here, I think of a similar approach, probably through the prototype chain to intercept the operation on the array, so as to realize the operation on the array of the behavior of listening.
The implementation is as follows:
ArrExtend inherits all properties of the Array first
const arrExtend = Object.create(Array.prototype)
const arrMethods = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
/** * arrExtend acts as an interception object and overwrites the methods in it */
arrMethods.forEach(method= > {
const oldMethod = Array.prototype[method]
const newMethod = function(. args) {
oldMethod.apply(this, args)
console.log(`${method}The method is executed)
}
arrExtend[method] = newMethod
})
export default {
arrExtend
}
Copy the code
The code you need to add to the defineReactive function is:
if (Array.isArray(value)) {
value.__proto__ = arrExtend
}
Copy the code
Test it: data.list.push(1)
Let’s look at the results:
The logic of the above code is clear at a glance, and it is also a simplification of the implementation ideas in Vue. Use the arrExtend object as an interceptor. First, let the object inherit all the properties of the Array itself, so that it does not affect the use of other properties of the Array itself, and then rewrite the corresponding function, that is, after the original method is called, notify other related dependencies that the property has changed. This is almost exactly what the setter does in Object.defineProperty, except you can specify which operation the user is doing, whether the length of the array changes, and so on.
Is there any other way?
In ES6, we saw a refreshing property called Proxy. Let’s take a look at the concept:
By calling new Proxy(), you create a Proxy to replace another object (called a target) that virtualizes the target object so that the Proxy and the target object can be treated ostensibly as the same object.
Proxies allow you to intercept low-level operations on target objects, which is the internal capability of the JS engine. Interception uses a function (called a trap) that responds to a particular operation.
Proxy, as the name suggests, is a feature that allows us to play around with objects. When we use Proxy to Proxy an object, we will get an object almost exactly the same as the proxied object, and this object can be completely monitored.
What do you mean by complete monitoring? What the Proxy provides is interception of the underlying operations. We used Object.defineProperty when we implemented listening on objects. This is actually a high-level operation provided by JS, which is exposed through low-level encapsulation. The power of proxies is that we can directly intercept the underlying operations on Proxy objects. In this way, we are listening to an object from the bottom of the operation.
Can we improve our code?
const createProxy = data= > {
if (typeof data === 'object' && data.toString() === '[object Object]') {
for (let k in data) {
if (typeof data[k] === 'object') {
defineObjectReactive(data, k, data[k])
} else {
defineBasicReactive(data, k, data[k])
}
}
}
}
function defineObjectReactive(obj, key, value) {
/ / recursion
createProxy(value)
obj[key] = new Proxy(value, {
set(target, property, val, receiver) {
if(property ! = ='length') {
console.log('Set %s to %o', property, val)
}
return Reflect.set(target, property, val, receiver)
}
})
}
function defineBasicReactive(obj, key, value) {
Object.defineProperty(obj, key, {
enumerable: true.configurable: false,
get() {
return value
},
set(newValue) {
if (value === newValue) return
console.log(` found${key}attribute${value} -> ${newValue}`)
value = newValue
}
})
}
export default {
createProxy
}
Copy the code
For attributes of the base type in an Object, we still implement responsive attributes through Object.defineProperty, because there is no pain point there, but when I implement listening for attributes of Object type, I create a proxy. Because our pain point was not being able to effectively listen for changes in the array. When we use this improved method, we don’t need to overwrite the array to listen for the array operations as we did before. This method has many limitations. We can’t overwrite all the array operations, and we can’t respond to operations like data.array.length = 0. After the proxy implementation, everything is different. We can implement a low-level implementation to listen for changes in the array. You can even watch changes in array length and all sorts of more detailed things. This certainly solves a big problem.
Let’s call the method we just did. Let’s see?
let data = {
name: 'Jiang'.userInfo: {
gender: 0.movies: []},list: []
}
createProxy(data)
data.name = 'Solo'
data.userInfo.gender = 0
data.userInfo.movies.push('Interstellar')
data.list.push(1)
Copy the code
The output is:
The result is very good. We implemented a Proxy to listen for all the properties of the object. There is a lot more to Proxy operations, such as putting the Proxy as a prototype on the prototype chain. Proxy can be more widely used, and there are many scenarios. This is also my first time to use it, so it needs to be consolidated. ´ Д `)