監聽數組變化,實現響應


舉個例子,來說明下為什么監聽不到數組變化

var target ={
    val: 1
}

let _value = target.val
Object.defineProperty(target,"val",{
    get:function(){
        return _value
    },
    set:function(newVal){
        _value = newVal
        console.log("setted")
    }
})

console.log(target.val)  // 1
console.log(target.val = [])  // setted []
console.log(target.val = [1,2,3])  // setted [1,2,3]
console.log(target.val[1]=10)    // 10
console.log(target.val.push(8))   // 4
console.log(target.val.length=5)  // 5
console.log(target.val)  // [1, 10, 3, 8, empty]

從本例中可以看到,當taget.val被設置為數組后,想要對數組內部進行修改,通過數組索引去賦值 target.val[1]=10 ,不會觸發set方法執行。

那么該如何實現呢?

我們先來了解下 Array.prototype.push.call() 相關知識,便於監聽數組,實現響應做鋪墊。

Array.prototype.push.apply() 

var a = [1,2,3];
var b = [4,5,6];

Array.prototype.push.apply(a, b);
console.log(a) //[1,2,3,4,5,6]

原生push方法接受的參數是一個參數列表,它不會自動把數組擴展成參數列表,使用apply的寫法可以將數組型參數擴展成參數列表,這樣合並兩個數組就可以直接傳數組參數了。

注:合並數組為什么不直接使用Array.prototype.concat()呢?

因為concat不會改變原數組,concat會返回新數組,而上面apply這種寫法直接改變數組a。

簡單實現監聽數組變化

let arr = [1,2,3]
console.log(arr)

打印結果看來,數組的隱式原型上掛載了一些方法,如push()、pop()、shift()、unshift()、splice()、sort()、reverse()等。

我們重新改寫下方法

let arr = [1,2,3]
arr.__proto__ = {
    push: function() {
        // 這里的this指arr,即[1,2,3]
        return Array.prototype.push.apply(this, arguments)
    }
}
arr.push(6)
console.log('修改后數組:',arr)

在官方文檔,所需監視的只有 push()、pop()、shift()、unshift()、splice()、sort()、reverse() 7 種方法。我們可以遍歷一下:

只需要監聽我們需要監聽的數據數組的一個變更,而不是針對原生Array的一個重新封裝。 

會重寫Array.prototype.push方法,並生成一個新的數組賦值給數據,這樣數據雙向綁定就會觸發。

首先讓這個對象繼承 Array 本身的所有屬性,這樣就不會影響到數組本身其他屬性的使用,后面對相應的函數進行改寫,也就是在原方法調用后去通知其它相關依賴這個屬性發生了變化,這點和 Object.defineProperty 中 setter所做的事情幾乎完全一樣,唯一的區別是可以細化到用戶到底做的是哪一種操作,以及數組的長度是否變化

不會污染到原生Array上的原型方法。

首先我們將需要監聽的數組的原型指針指向newArrProto,然后它會執行原生Array中對應的原型方法,與此同時執行我們自己重新封裝的方法。

那么問題來了,這種形式咋這么眼熟呢?這不就是我們見到的最多的繼承問題么?子類(newArrProto)和父類(Array)做的事情相似,卻又和父類做的事情不同。但是直接修改__proto__隱式原型指向總感覺心里怪怪的(因為我們可能看到的多的還是prototype),心里不(W)舒(T)服(F)。

 

const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
const newArrProto = [];
['push', 'pop','shift','unshift','splice','sort','reverse'].forEach(method => {
  // 原生Array的原型方法
  let original = arrayMethods[method];
  // 將push,pop等方法重新封裝並定義在對象newArrProto的屬性上
  // 注:封裝好的方法是定義在newArrProto的屬性上而不是其原型屬性,即newArrProto.__proto__ 沒有改變
  newArrProto[method] = function mutator() {
    console.log('監聽到數組的變化啦!');
   // 更新視圖,dep.notify()
// 調用對應的原生方法並返回結果(新數組長度) return original.apply(this, arguments); } }) let list1 = [1, 2]; // 將我們要監聽的數組的原型指針指向上面定義的空數組對象 // newArrProto的屬性上定義了我們封裝好的push,pop等方法 list1.__proto__ = newArrProto; list1.push(3); // 監聽到數組的變化啦! 3 // list2沒有被重新定義原型指針,所以這里會正常執行原生Array上的原型方法 let list2 = [1, 2]; list2.push(3); // 3

Array.prototype.push.call() 

var obj = {}
console.log(Array.prototype.push.call(obj, 'a','b','c')) // 3
console.log(obj) // {0: "a", 1: "b", 2: "c", length: 3}

var obj1 = {
    length: 5
}
console.log(Array.prototype.push.call(obj1, 'a','b','c')) // 8
console.log(obj1) // {5: "a", 6: "b", 7: "c", length: 8}

var obj2 = {
    0: 'e',
    1: 'f',
    length: 7
}
console.log(Array.prototype.push.call(obj2, 'a','b','c')) // 10
console.log(obj2) // {0: "e", 1: "f", 7: "a", 8: "b", 9: "c", length: 10}

通過上面對比結果,我們可以看出:

1)當對象中不含有length屬性時,調用數組原型方法push,將對象轉為類數組對象,新增屬性的索引從0開始,且lengt指是新增屬性的個數

2)當對象中含有length屬性時,新增屬性的索引命名從length長度開始計算。 

eg: obj1中length為5,新增加屬性的索引分別為5、6、7;obj2中length為7,新增加屬性的索引分別為7、8、9 

Array.prototype.slice.call()

Array.prototype.slice.call()方法是只能在類數組上起作用的,並不能同push()方法一樣可以可以使對象轉換為帶有length屬性的類數組對象。

 

結論,當對象中沒有length屬性時,默認添加的新屬性索引應為0,因為a中已經有為0的key了,於是將原來的banana覆蓋了,便有了現在的結果。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM