Vue3.x已经发布一段时间了,更新内容也是比较多的,但是比较让我感兴趣的还是如何对Vue2无法直接监测数组变化的优化,今天抽时间来简单看一下实现原理。
首先从表象上来看,Vue2对数组的响应式实现是有些不足的:
先来简单分析下为什么会存在上述问题:
我们知道Vue2是通过Object.defineProperty方法来进行数据监测的,但是这个方法是无法监测到数据的变化吗?其实并不是,我们来看下下面的例子:
// 设置对象
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log('获取数据', key, value);
return value;
},
set: function(newVal) {
console.log('设置数据', key, newVal);
value = newVal;
}
});
}
// 为每个属性设置代理(Vue源码中的walk方法)
function observe(data) {
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
}
let testArr = ['a', 'b', 'c'];
observe(testArr);
testArr[0]; // "获取数据" "0" "a"
testArr[0] = 1;
从上述结果可以看出,Object.defineProperty是可以对数组进行监测的,但是Vue2为什么没用呢,其实是出于性能的考虑,数据一般会被频繁的改动,每次的改动都需要遍历整个数组,给数组属性重新observe,这样会极大的消耗性能,因此在Vue2中hack了Array上的一些方法。
https://github.com/vuejs/vue/issues/8562
/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */
import {
def } from '../util/index';
// 复制数组构造函数的原型,Array.prototype也是一个数组。
const arrayProto = Array.prototype
// 创建对象,对象的__proto__指向arrayProto,所以arrayMethods的__proto__包含数组的所有方法。
export const arrayMethods = Object.create(arrayProto)
// 下面的数组是要进行重写的方法
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 遍历methodsToPatch数组,重写其中的方法
methodsToPatch.forEach(function (method) {
// 缓存原有方法
const original = arrayProto[method]
// def方法定义在lang.js文件中,是通过object.defineProperty对属性进行重新定义。
// 即在arrayMethods中找到我们要重写的方法,对其进行重新定义
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
// ,对于push,unshift会新增索引,所以需要手动observe
case 'push':
case 'unshift':
inserted = args
break;
// splice方法,如果传入了第三个参数,也会有新增索引,所以也需要手动observe
case 'splice':
inserted = args.slice(2)
break;
}
// push,unshift,splice触发后,手动observe,其他方法的变更会在当前的索引上进行更新,不需要执行observe
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify()
return result
})
})
所以数组调用以上这些方法的时候是会触发数据绑定的变化的。
另外,$set也检测数组变动,看下源码:
function set (target, key, val) {
//...
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
//...
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val
}
同样的道理,只是一个数组的判断,之后去observe,然后notify。
Vue3是用Proxy来进行对象、数组的代理,其实只要理解了Proxy就明白Vue3为什么会抛弃Object.defineProperty了:
const handler = {
set(target, key, value) {
console.log('set', key);
target[key] = value;
return true;
},
get(target, key) {
console.log('get', key);
return target[key];
}
};
const target = ['a', 'b', 'c'];
const proxy = new Proxy(target, handler);
proxy[0] = 'li';
proxy[1];
console.log(target);
仔细分析不难看出,Object.defineProperty与Proxy的区别在于一个是只能对属性拦截,对象或数组监测时必须遍历,另一个是对对象本身的劫持,确实方便了很多,但是Proxy本身的支持可能还是要看浏览器的支持程度的,不过Vue3也可能对其做了polyfill,可以拉下源码再瞅瞅。