数组可以用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)
}
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