Vue implements two-way data binding through data hijacking, using Object.defineProperty()
to hijack properties. However, the setter
in Object.defineProperty()
cannot directly monitor changes in array values. To monitor direct access to array indexes, each value needs to be hijacked. However, due to performance considerations, Vue does not adopt this approach, hence the need for special handling of array changes.
Vue achieves two-way data binding through data hijacking, with the core method being Object.defineProperty()
, which allows precise addition or modification of object properties. The getter
and setter
descriptor in the property descriptor is used for hijacking.
var obj = { __x: 1 };
Object.defineProperty(obj, "x", {
set: function(x){ console.log("watch"); this.__x = x; },
get: function(){ return this.__x; }
});
obj.x = 11; // watch
console.log(obj.x); // 11
When hijacking is applied to an array and values in the array are directly accessed by index, the setter
in Object.defineProperty()
cannot directly monitor changes in the array values. Therefore, special handling of array changes is required. While it's possible to loop through each value in the array and use Object.defineProperty()
to hijack them by index, Vue's creator, Evan You, explained that due to the performance cost not being proportional to the user experience benefits, this method was not used to make index access responsive. Specific details can be found in the Vue source code on GitHub
in issue #8562
.
var obj = { __x: [1, 2, 3] };
Object.defineProperty(obj, "x", {
set: function(x){ console.log("watch"); this.__x = x; },
get: function(){ return this.__x; }
});
obj.x[0] = 11;
console.log(obj.x); // [11, 2, 3]
obj.x = [1, 2, 3, 4, 5, 6]; // watch
console.log(obj.x); // [1, 2, 3, 4, 5, 6]
obj.x.push(7);
console.log(obj.x); // [1, 2, 3, 4, 5, 6, 7]
// Hijacking each value by index
var obj = { __x: [1, 2, 3] };
Object.defineProperty(obj, "x", {
set: function(x){ console.log("watch"); this.__x = x; },
get: function(){ return this.__x; }
});
obj.x.forEach((v, i) => {
Object.defineProperty(obj.x, i,{
set:function(x) { console.log("watch"); v = x; },
get: function(){ return v; }
})
})
obj.x[0] = 11; // watch
console.log(obj.x); // [11, 2, 3]
In Vue, data undergoes special processing, and direct index access modifications also do not trigger the setter
, but methods like push
have been overridden for this purpose.
<!DOCTYPE html>
<html>
<head>
<title>Listening for Array Changes in Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
msg: [1, 2, 3]
},
template:`
<div>
<div v-for="item in msg" :key="item">{{item}}</div>
<button @click="subscript">subscript</button>
<button @click="push">push</button>
</div>
`,
methods:{
subscript: function(){
this.msg[0] = 11;
console.log(this.msg); // [11, 2, 3, __ob__: Observer]
},
push: function(){
this.msg.push(4, 5, 6);
console.log(this.msg); // [1, 2, 3, 4, 5, 6, __ob__: Observer]
}
}
})
</script>
</html>
The specific override solution in Vue
is completed through the prototype chain. Specifically, a new object is created using the Object.create
method, using the passed object as the __proto__
of the newly created object, and then intercepting operations on the array with specific methods to achieve the monitoring of array operations.
// dev/src/core/observer/array.js
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
The Object.defineProperty()
method cannot intercept changes to the values accessed through array index, so it is necessary to avoid this kind of access. You can use the method of modifying the value and then assigning it, or you can use some methods in the array to form a new array. Methods that do not change the original array and return a new array include slice
, concat
, and the spread operator. Of course, you can also use the map
method to generate a new array. In addition, in Vue
, because the splice
method has been rewritten, you can also use the splice
method to update the view.
var obj = { __x: [1, 2, 3] };
Object.defineProperty(obj, "x", {
set: function(x){ console.log("watch"); this.__x = x; },
get: function(){ return this.__x; }
});
obj.x[0] = 11;
obj.x = obj.x; // watch
console.log(obj.x); // [11, 2, 3]
obj.x[0] = 111;
obj.x = [].concat(obj.x); // watch
console.log(obj.x); // [111, 2, 3]
obj.x[0] = 1111;
obj.x = obj.x.slice(); // watch
console.log(obj.x); // [1111, 2, 3]
obj.x[0] = 11111;
obj.x = obj.x.splice(0, obj.x.length); // watch
console.log(obj.x); // [11111, 2, 3]
Vue 3.0
uses Proxy
to achieve data interception. Object.defineProperty
can only monitor properties, while Proxy
can monitor the entire object. By calling new Proxy()
, a proxy can be created to replace another object, which is called the target. This proxy virtually represents the target object, so the proxy and the target object can be treated as the same object on the surface. The proxy allows intercepting low-level operations on the target object, which is originally an internal ability of the Js
engine. The interception behavior uses a function that can respond to specific operations. By using Proxy
to proxy an object, we will get an object that is almost identical to the object being proxied and can completely monitor this object from a low level.
var target = [1, 2, 3];
var proxy = new Proxy(target, {
set: function(target, key, value, receiver){
console.log("watch");
return Reflect.set(target, key, value, receiver);
},
get: function(target, key, receiver){
return target[key];
}
});
proxy[0] = 11; // watch
console.log(target); // [11, 2, 3]
https://github.com/WindrunnerMax/EveryDay
https://zhuanlan.zhihu.com/p/50547367
https://github.com/vuejs/vue/issues/8562
https://juejin.im/post/6844903699425263629
https://juejin.im/post/6844903597591773198
https://segmentfault.com/a/1190000015783546
https://cloud.tencent.com/developer/article/1607061
https://www.cnblogs.com/tugenhua0707/p/11754291.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy