數組可以用defineProperty
進行監聽。但是考慮性能原因,不能數組一百萬項每一項都循環監聽(那樣性能太差了)。所以沒有使用Ojbect.defineProperty對數組每一項進行攔截,而是選擇劫持數組原型上的個別方法並重寫。
具體重寫的有:
push
、pop
、shift
、unshift
、sort
、reverse
、splice
(這七個都是會改變原數組的)
另外要注意的是:
不是直接粗暴重寫了Array.prototype
上的push等方法,而是通過原型鏈繼承與函數劫持進行的移花接木。並且只監聽調用了defineReactive
函數時傳進來的數組。
具體實現思路:
以push為例,而是利用Object.create(Array.prototype)
生成新的數組對象,該對象的__proto__指向Array.prototype。並在對象身上創建push等函數,利用「函數劫持」,在函數內部Array.prototype.push.call調用原有push方法,並執行自己劫持的代碼(如視圖更新)。最后將需要綁定的數組的__proto__由指向Array.prototype改向指成擁有重寫方法的新數組對象。具體看下邊源碼仿寫,真實Array.prototype里的祖宗級別push等方法沒有動。
思考:
為啥不重寫map等也是修改原數組的方法呢?
特別注意:
在Vue中修改數組的索引和長度,是無法被監控到並做響應式視圖更新的。需要通過以上7種變異方法修改數組才會觸發數組對應watcher進行更新。 數組中如果是對象數據類型的也會進行遞歸劫持。 如果情節需要,通過索引來修改數組里的內容。可以通過Vue.$set()
方法來進行處理,或者使用splice方法
實現。(其實$set內部的核心也是splice方法)
原理mock:
let state = [1,2,3]; //待監聽的數據
// 1、響應式數據-函數劫持實現數組原型方法重寫
let OriginalArray = Array.prototype;
/*
並不是直接改寫原型上的方法。而是給當前待監聽的數組
原型鏈上加了push等方法劫持了Array原型的push方法。
*/
let arrayMethods = Object.create(OriginalArray)
/*
創建一個新對象(對象or數組由第一個參數決定)
帶着指定的原型對象(Array.prototype)
*/
console.log(
arrayMethods,
// 原型修改
arrayMethods.__proto__ === OriginalArray,
arrayMethods.__proto__ === Array.prototype
)
function defineReactive(obj) {
// 【函數劫持】改寫這個新對象身上的push、splice等數組方法
arrayMethods.push = function(...args){
// 並還是調用原生的push方法
OriginalArray.push.apply(this, args) // 或者用call(this, ...args)
// 然后這里邊做自己的事情,比如視圖更新(具體源碼怎么更新的視圖?)
render()
}
//
obj.__proto__ = arrayMethods
/*
修改傳進來的、被監聽的數組的原型鏈,鏈接數組與被重寫的方法。
原本__proto__指向Array.prototype,現在中間給他包了一層,指向我們重寫的原型方法。
並在重寫的原型方法里再調用Array.prototype的同名原型方法。
*/
}
defineReactive(state);
// 操作dom
function render() {
app.innerHTML = state;
}
render()
// 更改數據,觀察dom修改
btn.onclick = () => {
state.push(state[state.length - 1] + 1)
}
源碼位置:
github:src/core/observer/array.js:8