跳到主要内容

Vue2 中数组的变更检测

在 Vue2 中,通过Object.defineProperty()实现数据的响应式,但它无法检测到数组内部元素的数据变动

var vm = {
data: {
a: 1,
b: 2,
list: [3, 4, 5, 6],
},
};

for (var key in vm.data) {
(function (k) {
Object.defineProperty(vm, k, {
get: function () {
console.log('数据的响应式获取');
return vm.data[k];
},
set: function (newValue) {
console.log('数据的响应式设置');
vm.data[k] = newValue;
},
});
})(key);
}

vm.a; // 输出:'数据的响应式获取'
vm.b = 3; // 输出:'数据的响应式设置'
vm.list = [1, 2, 3]; // 输出:'数据的响应式设置'
vm.list.push(999); // 无输出,无法触发setter

以下数组方法都无法触发setter

vm.list.push(6);
vm.list.pop();
vm.list.unshift();
vm.list.shift();
vm.list.splice(2, 1);
vm.list.sort((a, b) => a - b);
vm.list.reverse();

无法检测的原因

  1. Object.defineProperty()方法没办法监听到数组内部数据的变化。因为vm.list.push(999)并没有改变数组的引用,仅仅是改变了数组内部的元素。

  2. 这也是为什么 Vue3 中使用了Proxy来代替defineProperty的原因。Proxy可以拦截更多的操作,包括数组的修改。

通过包裹数组方法实现监听

为了解决无法监听数组内部变更的问题,Vue2 中采用了包裹数组方法的方式来实现:

var ArrayPrototype = Array.prototype;
var ARR_METHODS = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse'];

ARR_METHODS.forEach((method) => {
ArrayPrototype[method] = function (...args) {
const result = ArrayPrototype[method].apply(this, args);
console.log('触发视图更新');
return result;
};
});

通过修改数组原型上的方法,在调用这些方法时,除了执行原有逻辑外,还可以添加额外的逻辑,比如触发视图更新等。

替换数组是否会影响性能

直接替换整个数组不一定会影响性能,因为 Vue 在对 DOM 操作时会进行大量的新旧节点对比,尽可能地复用已有的 DOM 节点,将重新渲染的 DOM 数量最小化。例如:

const App = {
data() {
return {
list: [1, 2, 3, 4, 5],
};
},
template: `
<div>
<span v-for="n in list" :key="n">{{ n }}</span>
<button @click="operateList">Operate List</button>
</div>
`,
methods: {
operateList() {
this.list = this.list.concat(this.list[this.list.length - 1] + 1);
this.list = this.list.slice(2, 4);
this.list = this.list.map((item) => item + 1);
},
},
};

在点击按钮后,list数组经历了一系列的替换操作,但 Vue 会智能地分析新旧list的差异,尽可能地复用已有的 DOM 节点。所以整体的性能开销并不会很大。

当然,频繁地替换一个巨大的数组,还是会对性能产生一定影响。应该根据实际场景权衡,尽量避免不必要的整体替换。