一、问题的由来:为什么数组这么特殊?
让我们先来看一个常见的"坑":
javascript
// 假设我们有一个 Vue 实例
new Vue({
data() {
return {
items: ['苹果', '香蕉', '橙子']
}
},
created() {
// 这种修改方式,视图不会更新!
this.items[0] = '芒果';
this.items.length = 0;
// 这种修改方式,视图才会更新
this.items.push('葡萄');
}
})
问题来了:为什么同样是修改数组,有的方式能触发更新,有的却不能?
二、Vue 2.x 的解决方案:拦截数组方法
1. 核心原理:方法拦截
Vue 2.x 中,通过重写数组原型上的 7 个方法来监听数组变化:
ini
// Vue 2.x 的数组响应式核心实现(简化版)
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
'push', 'pop', 'shift', 'unshift',
'splice', 'sort', 'reverse'
];
methodsToPatch.forEach(function(method) {
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 1. 先执行原始方法
const result = original.apply(this, args);
// 2. 获取数组的 __ob__ 属性(Observer 实例)
const ob = this.__ob__;
// 3. 处理新增的元素(如果是 push/unshift/splice 添加了新元素)
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
// 4. 对新元素进行响应式处理
if (inserted) ob.observeArray(inserted);
// 5. 通知依赖更新
ob.dep.notify();
return result;
});
});
// 在 Observer 类中
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
if (Array.isArray(value)) {
// 如果是数组,修改其原型指向
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
}
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
2. 支持的数组方法
Vue 能够检测变化的数组操作:
kotlin
// 以下操作都能被 Vue 检测到
this.items.push('新元素') // 末尾添加
this.items.pop() // 删除最后一个
this.items.shift() // 删除第一个
this.items.unshift('新元素') // 开头添加
this.items.splice(0, 1, '替换值') // 替换元素
this.items.sort() // 排序
this.items.reverse() // 反转
3. 无法检测的变化
kotlin
// 以下操作无法被检测到
this.items[index] = '新值'; // 直接设置索引
this.items.length = 0; // 修改长度
// 解决方案:使用 Vue.set 或 splice
Vue.set(this.items, index, '新值');
this.items.splice(index, 1, '新值');
三、实战代码示例
让我们通过一个完整的例子来理解:
xml
<template>
<div>
<h3>购物清单</h3>
<ul>
<li v-for="(item, index) in shoppingList" :key="index">
{{ item }}
<button @click="removeItem(index)">删除</button>
<button @click="updateItem(index)">更新</button>
</li>
</ul>
<input v-model="newItem" placeholder="输入新商品">
<button @click="addItem">添加商品</button>
<button @click="badUpdate">错误更新方式</button>
<button @click="goodUpdate">正确更新方式</button>
</div>
</template>
<script>
export default {
data() {
return {
shoppingList: ['牛奶', '面包', '鸡蛋'],
newItem: ''
}
},
methods: {
addItem() {
if (this.newItem) {
// 正确方式:使用 push
this.shoppingList.push(this.newItem);
this.newItem = '';
}
},
removeItem(index) {
// 正确方式:使用 splice
this.shoppingList.splice(index, 1);
},
updateItem(index) {
const newName = prompt('请输入新的商品名:');
if (newName) {
// 正确方式:使用 Vue.set 或 splice
this.$set(this.shoppingList, index, newName);
// 或者:this.shoppingList.splice(index, 1, newName);
}
},
badUpdate() {
// 错误方式:直接通过索引修改
this.shoppingList[0] = '直接修改的值';
console.log('数据变了,但视图不会更新!');
},
goodUpdate() {
// 正确方式
this.$set(this.shoppingList, 0, '正确修改的值');
console.log('数据和视图都会更新!');
}
}
}
</script>
四、流程图解:Vue 数组响应式原理
markdown
开始
│
▼
初始化数据
│
▼
Observer 处理数组
│
▼
修改数组原型链
│
├─────────────────┬─────────────────┐
▼ ▼ ▼
设置 __ob__ 属性 重写7个方法 建立依赖收集
│ │ │
▼ ▼ ▼
当数组方法被调用时
│
├───────────────┐
▼ ▼
执行原始方法 收集新元素
│ │
▼ ▼
新元素响应式处理
│
▼
通知所有依赖更新
│
▼
触发视图重新渲染
│
▼
结束
五、Vue 3 的进步:Proxy 的魔力
Vue 3 使用 Proxy 重写了响应式系统,完美解决了数组检测问题:
typescript
// Vue 3 的响应式实现(简化版)
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 如果获取的是数组或对象,继续代理
if (typeof res === 'object' && res !== null) {
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 判断是新增属性还是修改属性
const type = Array.isArray(target)
? Number(key) < target.length ? 'SET' : 'ADD'
: Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD';
// 触发更新
trigger(target, key, type, value, oldValue);
return result;
}
});
}
// 现在这些操作都能被检测到了!
const arr = reactive(['a', 'b', 'c']);
arr[0] = 'x'; // ✅ 可以被检测
arr.length = 0; // ✅ 可以被检测
arr[3] = 'd'; // ✅ 新增索引可以被检测
六、最佳实践总结
在 Vue 2 中:
-
- 使用变异方法:push、pop、shift、unshift、splice、sort、reverse
-
- 修改特定索引 :使用
Vue.set()或vm.$set()
- 修改特定索引 :使用
-
- 修改数组长度 :使用
splice
- 修改数组长度 :使用
在 Vue 3 中:
由于使用了 Proxy,几乎所有数组操作都能被自动检测,无需特殊处理。
实用工具函数:
scss
// 创建一个数组修改工具集
const arrayHelper = {
// 安全更新数组元素
update(array, index, value) {
if (Array.isArray(array)) {
if (this.isVue2) {
Vue.set(array, index, value);
} else {
array[index] = value;
}
}
},
// 安全删除数组元素
remove(array, index) {
if (Array.isArray(array)) {
array.splice(index, 1);
}
},
// 清空数组
clear(array) {
if (Array.isArray(array)) {
array.splice(0, array.length);
}
}
};
七、常见问题解答
Q:为什么 Vue 2 不直接监听数组索引变化?
A:主要是性能考虑。ES5 的 Object.defineProperty 无法监听数组索引变化,需要通过重写方法实现。
Q:Vue.set 内部是怎么实现的?
A:Vue.set 在遇到数组时,本质上还是调用 splice 方法。
Q:为什么直接修改 length 不生效?
A:因为 length 属性本身是可写的,但改变 length 不会触发 setter。
结语
理解 Vue 如何检测数组变化,是掌握 Vue 响应式系统的关键一步。从 Vue 2 的方法拦截到 Vue 3 的 Proxy 代理,技术的进步让开发者体验越来越好。记住核心原则:在 Vue 2 中,始终使用变异方法修改数组;在 Vue 3 中,你可以更自由地操作数组。
希望这篇文章能帮助你彻底理解 Vue 的数组响应式原理!如果你有更多问题,欢迎在评论区留言讨论。