JavaScript如何監測數組的變化
前言
之前介紹defineProperty的時候說到,其隻能監測對象的變化,並不能監測數組的變化。
本文致力於說清楚怎麼實現監測數組的變化。
核心思路:找到改變原數組的方法,然後對這些方法進行劫持處理。
上面這句話,是重中之重,務必讀三遍,記住瞭,再往下走。
改變原數組,常用到的方法有push pop shift unshift reverse sort splice。
換言之,這些方法是改變數組的入口。
在數組實例和數組原型之間,加一個新的原型
直接修改Array.prototype,是極其危險的。
換一個思路,復制已有的數組原型,然後修改其中的方法,但這裡因為原型上的方法不可枚舉,自然也就沒法復制。
於是再換一個思路,在數組和數組的原型之間,插入一個原型,形成原型鏈,數組 => 新原型 => 數組的原型,可以在新原型上增加同名的方法就可以瞭。
先借助偽代碼理解:
// 偽代碼 let arr = []; arr.__proto__ = newPrototype; newPrototype.__proto__ = Array.prototype; // 然後可以在新原型上添加同名的方法就可以瞭 newPrototype.push = xxx;
轉換成真實的代碼如下,核心使用瞭Object.create,
// Object.create返回一個新對象,而來新對象的__proto__就是傳進去的參數。 let newPrototype = Object.create(Array.prototype); // 然後可以在新原型上添加同名的方法就可以瞭 newPrototype.push = xxx; // 需要監測的數組,綁定新的原型就可以瞭 let arr = []; arr.__proto__ = newPrototype;
以 push 為例,劫持 push
就是在新原型上重新寫一個 push 的方法,裡面執行老的 push,但除此之外還可以做點別的事。
let newPrototype = Object.create(Array.prototype); // 在新原型上添加同名push newPrototype.push = function(...args) { // 語義化this let curArr = this; console.log("使用瞭push"); // 最後還是會執行原始的push return Array.prototype.push.call(curArr, ...args); }; // 需要監測的數組,綁定新的原型就可以瞭 let arr = []; arr.__proto__ = newPrototype; // 執行push的時候,就會打印瞭 arr.push(1);
然後其他的方法也是類似的,試著寫寫其他的方法
劫持其他的方法
將其他方法也一起寫瞭,因為邏輯一樣,直接遍歷即可。
let newPrototype = Object.create(Array.prototype); let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"]; methods.forEach(method => { newPrototype[method] = function(...args) { console.log(`使用瞭${method}`); return Array.prototype[method].call(this, ...args); }; }); // 需要監測的數組,綁定新的原型就可以瞭 let arr = []; arr.__proto__ = newPrototype; // 執行的時候,就會打印瞭 arr.push(1); arr.pop();
數組裡有數組項的話,也是需要監測的
這裡數組裡可能也是有數組的,需要遍歷數組的每項,如果是數組的話依然需要指向新的原型。
嗯,對,用到遞歸瞭。
let newPrototype = Object.create(Array.prototype); let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"]; methods.forEach(method => { newPrototype[method] = function(...args) { console.log(`使用瞭${method}`); return Array.prototype[method].call(this, ...args); }; }); function observeArr(arr) { // 既是條件限制,也是遞歸的終止條件 if (!Array.isArray(arr)) { return; } // 整個數組指向新的原型 arr.__proto__ = newPrototype; // 數組的每項,如果是數組,也指向新的原型。 arr.forEach(observeArr); } // 需要監測的數組,綁定新的原型就可以瞭 let arr = [[1, 2, 3]]; observeArr(arr); // 執行的時候,就會打印瞭 arr[0].push(1); arr[1].pop();
數組新添加的項,如果是數組也需要指向新的原型
能添加元素的方法:push unshift splice。
找到新加的元素,然後是數組的也同樣指向新的原型
let newPrototype = Object.create(Array.prototype); let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"]; methods.forEach(method => { newPrototype[method] = function(...args) { console.log(`使用瞭${method}`); let inserted; switch (method) { case "push": case "unshift": inserted = args; break; case "splice": inserted = args.slice(2); break; default: break; } inserted && observeArr(inserted); return Array.prototype[method].call(this, ...args); }; }); function observeArr(arr) { // 即是條件限制,也是遞歸的終止條件 if (!Array.isArray(arr)) { return; } // 整個數組指向新的原型 arr.__proto__ = newPrototype; // 數組的每項,如果是數組,也指向新的原型。 arr.forEach(observeArr); } // 這裡可以導出去,方便別的文件使用 export default observeArr; // 需要監測的數組,綁定新的原型就可以瞭 let arr = []; observeArr(arr); let addItem = [1, 2, 3]; arr.push(addItem); // 執行的時候,就會打印瞭 addItem.push(1); addItem.pop();
綜合使用 defineProperty 監測對象和數組
現在已經有瞭監測對象的方法,也有瞭監測數組的方法,將兩個綜合起來,就能監測數組裡面的對象,對象裡面的數組瞭。
將監測數組和監測對象的的可以單獨寫成一個文件,方便之後使用。
這裡為瞭方便直接運行代碼,直接放在一塊瞭。
/** * observeArr的部分 **/ // 生成新的原型 let newPrototype = Object.create(Array.prototype); let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"]; // 在新原型上面添加以上方法,實現劫持 methods.forEach(method => { newPrototype[method] = function(...args) { console.log(`使用瞭${method}`); let inserted; switch (method) { case "push": case "unshift": inserted = args; break; case "splice": inserted = args.slice(2); break; default: break; } inserted && observeArr(inserted); return Array.prototype[method].call(this, ...args); }; }); function observeArr(arr) { // 新加!!!是對象的話,需要用對象 if (Object.prototype.toString.call(arr) === "[object Object]") { observeObj(arr); return; } if (Array.isArray(arr)) { // 整個數組指向新的原型 arr.__proto__ = newPrototype; // 數組的每項,如果是數組,也指向新的原型。 arr.forEach(observeArr); } // 不是對象或者數組的,什麼都不做 } /** * observeObj的部分 **/ function observeObj(obj) { // 加上參數限制,必須是對象才有劫持,也是遞歸的終止條件 if (typeof obj !== "object" || obj == null) { return; } // 新加!!!數組交給數組處理 if (Array.isArray(obj)) { observeArr(obj); return; } // 是對象的話 才開始遞歸 for (let key in obj) { // 直接使用 obj.hasOwnProperty會提示不規范 if (Object.prototype.hasOwnProperty.call(obj, key)) { observeKey(obj, key); // 這裡劫持該屬性的屬性值,如果不是對象直接返回,不影響 observeObj(obj[key]); } } return obj; } function observeKey(obj, key) { let value = obj[key]; Object.defineProperty(obj, key, { get() { console.log("讀取屬性", value); return value; }, set(newValue) { console.log("設置屬性", newValue); value = newValue; } }); } /** * demo試試 **/ let data = { a: 1, b: [1, 2, { c: 2 }] }; observeObj(data); data.a = 2; data.b.push([2, 3]); let arr = [{ a: "數組裡的對象" }, 3, 4]; observeArr(arr); arr[0].a = 3;
缺陷
當然數組其實可以不通過方法改變,比如直接刪除數組可以直接使用 length 屬性,或者直接arr[0]=xxx改變數組。
但隻有當使用”push”,”pop”, “shift”,”unshift”,”reverse”,”sort”,”splice”才能檢測到數組變化。
這也是 vue 的缺陷,當然新版的 proxy 將幹掉這個缺陷。
所以在使用 vue 的過程中,要盡量使用以上方法操作數組~~~
附註:查看數組的所有屬性和方法
在控制臺可以輸入dir([]),然後能看到數組所有的屬性和方法。
具體用法,可以直接到到mdn 上細看,點擊側邊欄看對應的方法
總結
到此這篇關於JavaScript如何監測數組變化的文章就介紹到這瞭,更多相關JS監測數組變化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!