Vue中數組變動監聽
Vue
的通過數據劫持的方式實現數據的雙向綁定,即使用Object.defineProperty()
來實現對屬性的劫持,但是Object.defineProperty()
中的setter
是無法直接實現數組中值的改變的劫持行為的,想要實現對於數組下標直接訪問的劫持需要使用索引對每一個值進行劫持,但是在Vue
中考慮性能問題並未采用這種方式,所以需要特殊處理數組的變動。
描述
Vue
是通過數據劫持的方式來實現數據雙向數據綁定的,其中最核心的方法便是通過Object.defineProperty()
來實現對屬性的劫持,該方法允許精確地添加或修改對象的屬性,對數據添加屬性描述符中的getter
與setter
存取描述符實現劫持。
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
而如果當劫持的值為數組且直接根據下標處理數組中的值時,Object.defineProperty()
中的setter
是無法直接實現數組中值的改變的劫持行為的,所以需要特殊處理數組的變動,當然我們可以對於數組中每一個值進行循環然后通過索引同樣使用Object.defineProperty()
進行劫持,但是在Vue
中尤大解釋說是由於性能代價和獲得的用戶體驗收益不成正比,所以並沒有使用這種方式使下標訪問實現響應式,具體可以參閱github
中Vue
源碼的#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]
// 通過下標對每一個值進行劫持
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]
在Vue
中對於數據是經過特殊處理的,對於下標直接訪問的修改同樣是不能觸發setter
,但是對於push
等方法都進行了重寫。
<!DOCTYPE html>
<html>
<head>
<title>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>
在Vue
中具體的重寫方案是通過原型鏈來完成的,具體是通過Object.create
方法創建一個新對象,使用傳入的對象來作為新創建的對象的__proto__
,之后對於特定的方法去攔截對數組的操作,從而實現對操作數組這個行為的監聽。
// 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
})
})
處理方法
重賦值
Object.defineProperty()
方法無法劫持對於數組值下標方式訪問的值的改變,這樣的話就需要避免這種訪問,可以采用修改后再賦值的方式,也可以采用數組中的一些方法去形成一個新數組,數組中不改變原數組並返回一個新數組的方法有slice
、concat
等方法以及spread
操作符,當然也可以使用map
方法生成新數組,此外在Vue
中由於重寫了splice
方法,也可以使用splice
方法進行視圖的更新。
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]
Proxy
Vue3.0
使用Proxy
實現數據劫持,Object.defineProperty
只能監聽屬性,而Proxy
能監聽整個對象,通過調用new Proxy()
,可以創建一個代理用來替代另一個對象被稱為目標,這個代理對目標對象進行了虛擬,因此該代理與該目標對象表面上可以被當作同一個對象來對待。代理允許攔截在目標對象上的底層操作,而這原本是Js
引擎的內部能力,攔截行為使用了一個能夠響應特定操作的函數,即通過Proxy
去對一個對象進行代理之后,我們將得到一個和被代理對象幾乎完全一樣的對象,並且可以從底層實現對這個對象進行完全的監控。
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