Vue2数组响应式问题:Object.defineProperty不能监听数组吗
一、官方文档说明
深入响应式原理 — Vue.js
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
举个例子:
var vm = new Vue({data: {items: ['a', 'b', 'c']} }) vm.items[1] = 'x' // 不是响应性的 vm.items.length = 2 // 不是响应性的
为了解决第一类问题,以下两种方式都可以实现和
vm.items[indexOfItem] = newValue
相同的效果,同时也将在响应式系统内触发状态更新:
// Vue.set Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue)
你也可以使用 vm.$set 实例方法,该方法是全局方法
Vue.set
的一个别名:
vm.$set(vm.items, indexOfItem, newValue)
为了解决第二类问题,你可以使用
splice
:
vm.items.splice(newLength)
根据官网文档:
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
二、Object.defineProperty监听数组
我们都知道Vue2的响应式是基于Object.defineProperty劫持对象属性然后定义get和set去实现的,难道是Object.defineProperty无法监听数组?,实际如何我们代码见分晓:
// const arr = new Array(100).fill().map((i, index) => index + 1);
const arr = ['a', 'b', 'c', 'd'];
arr.forEach((i, index) => {
Object.defineProperty(arr, index, {
get() {
console.log('get===>', i);
return i;
},
set(newValue) {
console.log('set===>', newValue);
i = newValue;
},
});
});
arr[0]; // 打印:get===> a
arr[3]; // 打印:get===> d
arr[4]; // 没有触发监听
arr[0] = 99; // 打印:set===> 99
arr[5] = 88; // 没有触发监听
arr.push(88); // 没有触发监听
arr.pop(); // 打印:get===> d
arr.length = 1 // 没有触发监听
arr.shift();
// 打印:
get===> a
get===> b
set===> b
get===> c
set===> c
get===> d
set===> d
arr.unshift(99);
// 打印:
get===> d
get===> c
set===> c
get===> b
set===> b
get===> a
set===> a
set===> 99
arr.splice(1)或arr.slice(1);
// 打印:
get===> b
get===> c
get===> d说明:删除获取截取数组下标1及1后所有
arr.splice(4)或arr.slice(4); // 不触发监听
arr.splice(0, 1);
// 打印:
get===> a
get===> b
set===> b
get===> c
set===> c
get===> d
set===> d说明:删除下标0,触发下标0get,后面元素往前补上导致索引变化索引触发get、set
arr.splice(0, 1, 99);
// 打印:
get===> a
set===> 99说明:下标0变化,所以触发了下标0的get,set
三、结论
由上面代码多方面验证,我们得出结论:Object.defineProperty是可以劫持并监听数组变化的,因为数组也是对象,它的属性就是数组下标,而因为Object.defineProperty只能劫持监听对象已有属性,所以对于arr[4]、arr[5] = 88、arr.push(88)、arr.splice(4)、arr.slice(4);这些没有超出数组长度、新增下标,没有改变数组原有元素下标的操作都不会触发监听
反之,如arr.splice(0, 1)、arr[0] = 99、arr.pop();、arr.unshift(99);这类修改数组原有元素或者破环数组下标顺序的操作会触发监听
四、Vue是如何实现数组的响应式的
那么由上面的结论得知Object.defineProperty是可以劫持数组并通过get、set监听原因元素变化的,但是Vue2并没有利用,因为:
- 性能问题:数组可能非常大,为每个索引设置 getter 和 setter 会极大地影响性能。
- 动态性问题:数组长度是动态变化的,每次数组变化时都需要重新为新的索引设置劫持,这在技术上是复杂且低效的。
length问题:
直接修改数组的length
属性(例如,通过设置arr.length = 0
来清空数组),这种操作同样无法被Object.defineProperty
直接侦测到。这是因为length
属性的变化不会触发索引属性的 setter。
正因为上述限制,Vue 2 选择了一种不同的方式来实现对数组的响应式监听:
重写数组方法:Vue 2 通过修改数组实例的原型,将数组的一些方法(如 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' 等)重写为可以触发视图更新的版本。当这些重写的方法被调用时,Vue 可以捕获到数组的变动并触发相应的更新。
总结来说,Object.defineProperty 由于其内在的机制和限制,并不能直接用于有效监听数组的变化。Vue 2 通过一种巧妙的方式绕过了这些限制,能够实现对数组操作的响应式更新。这种方法虽然巧妙,但有其局限性,比如直接通过索引设置数组元素的值或修改数组长度等操作,是无法被检测到的。