Vue中對數據的監聽主要依靠Object.defineProperty
來實現的,這種實現主要針對key/value
形式的對象,對數組中的值的變化是無能爲力的,definrProperty
是無法監聽數組長度的變化,監聽索引的代價也很高,那麼應該怎麼對數組中的數據進行監聽呢?
一、數組的變化情況:
- 數組本身的賦值
- 數組中
push
等方法導致的變化 - 數組中的值變化
- 操作數組的長度導致的變化
二、對上面的變化依次分析:
數組本身的賦值
這種情況和對象的監聽是一致的,直接使用defineProperty
對數據進行監聽就可以了。
數組中push等方法導致的變化
數組push
等操作改變數據時想要監聽數據的變化就沒有辦法通過defineProperty
來實現了。那要怎麼實現?
- 需要通過
Object.create
實現一個Array.prototype
的繼承者arraymethods
。它訪問的方法和Array.prototype上的是一樣的。我們不能直接在Array.prototype上對方法進行監聽,因爲這樣會影響到正常方法的調用。 - 將push等方法,通過
defineproperty
直接寫入到arraymethods
對象上:
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'reverse',
'sort'
];
const arrayProto = Array.prototype, //緩存Array的原型
arrayMethods = Object.create(arrayProto); //繼承Array的原型
//設置對象屬性的工具方法
function def(obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: true,
writable: true,
configurable: true
});
}
methodsToPatch.forEach(function(method, index) {
def(arrayMethods, method, function(...args) {
//arrayProto[method].apply(this, args);代表原來的方法
});
});
- 此時,如果直接訪問
arraymethods
上的七種方法,會直接訪問到arraymethods
對象上的,可以在裏面寫一些需要監聽的方法,內部通過arrayProto[method].apply(this,args)
訪問到數組上的真正的方法。 - 因爲
arraymethods
是我們構造出來的結構,它本身並不是數組,所以我們要對我們操作的數組通過以下方法讓其指向arraymethods
中的方法,而不是真正的Array.prototype
中的方法。
if('__proto__' in {}) {
//瀏覽器中有__proto__,將數組的原型指向arrayMethods,這樣當數組調用上述的7個方法時,其實是調用arrayMethods中的方法而不是調用Array.prototype中的方法
target.__proto__ = arrayMethods;
} else {
//如果瀏覽器不支持__proto__,那麼直接將arraymethods中的方法拷貝到數組實例中。則設置數組對應的屬性,這樣當數組調用上述的7個方法時,其實是調用數組對應屬性指向的方法
for(let i = 0, l = methodsToPatch.length; i < l; i++) {
let key = methodsToPatch[i];
def(target, key, arrayMethods[key]);
}
}
完整代碼:
const patchArray = (function() {
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'reverse',
'sort'
];
//設置對象屬性的工具方法
function def(obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: true,
writable: true,
configurable: true
});
}
const arrayProto = Array.prototype, //緩存Array的原型
arrayMethods = Object.create(arrayProto); //繼承Array的原型
methodsToPatch.forEach(function(method, index) {
def(arrayMethods, method, function(...args) {
//首先調用Array原型的方法
const old=this.concat([]);
const res = arrayProto[method].apply(this, args);
let inserted = null,
deleted = null;
let _callback_ = this._callback_;
//記錄插入的值
switch(method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
//這是新增的
inserted = args.slice(2);
let start = args[0],
end = start + args[1];
deleted = old.slice(start, end);
break;
case 'pop':
case 'shift':
deleted = res;
}
_callback_(inserted, deleted);
return res;
});
});
return function(target, callback) {
def(target, '_callback_', callback); //定義回調
//看看瀏覽器支不支持__proto__這個屬性,通過改變__proto__的值,可以設置對象的原型
if('__proto__' in {}) {
//將數組的原型指向arrayMethods,這樣當數組調用上述的7個方法時,其實是調用arrayMethods中的方法而不是調用Array.prototype中的方法
target.__proto__ = arrayMethods;
} else {
//如果瀏覽器不支持__proto__,則設置數組對應的屬性,這樣當數組調用上述的7個方法時,其實是調用數組對應屬性指向的方法
for(let i = 0, l = methodsToPatch.length; i < l; i++) {
let key = methodsToPatch[i];
def(target, key, arrayMethods[key]);
}
}
}
})();
//測試
let arr = [1, 2, 3];
patchArray(arr, function(add, del) {
if(add)
console.log('這是新增的內容:', add);
if(del)
console.log('這是刪除的內容:', del);
});
arr.splice(1,2,'aa','bb','cc')
參考文檔:http://www.qiutianaimeili.com/html/page/2019/05/m0kcbzlpc9s.html
https://www.cnblogs.com/DevinnZ/p/10569033.html