在 Vue 开发中,你是否思考过:
"为什么
this.items.push(newItem)
能自动更新页面?"
"Vue 是如何监听数组内部变化的?"
"sort()
和reverse()
为何也能触发视图更新?"
这背后是 Vue 对数组方法的精妙劫持与增强。
本文将从 Object.defineProperty
的局限 到 Vue 源码级实现,彻底解析 Vue 数组响应式的黑科技。
一、问题根源:Object.defineProperty
的缺陷
✅ Vue 2 响应式原理
Vue 2 使用 Object.defineProperty
劫持对象属性的 get
和 set
:
js
Object.defineProperty(obj, 'prop', {
get() { /* 依赖收集 */ },
set(newVal) { /* 派发更新 */ }
});
❌ 数组的"盲区"
js
const arr = ['a', 'b'];
arr[2] = 'c'; // ❌ 无法触发 set
arr.length = 0; // ❌ 无法触发 set
arr.push('d'); // ❌ 原生方法不走 set
💥
Object.defineProperty
无法监听:
- 数组索引赋值;
- 数组长度变化;
- 数组方法调用。
二、Vue 的解决方案:方法劫持
✅ 核心思路
"既然不能监听属性,那就劫持会改变数组的方法!"
Vue 创建了一个增强版数组原型,覆盖了 7 个会改变原数组的方法。
三、源码级实现:arrayMethods
详解
📌 步骤 1:创建增强原型
js
// 缓存原生 Array.prototype
const arrayProto = Array.prototype;
// 创建新对象,__proto__ 指向原生原型
export const arrayMethods = Object.create(arrayProto);
💡 这样既能保留原生功能,又能扩展新逻辑。
📌 步骤 2:拦截变异方法
js
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. 获取 Observer 实例
const ob = this.__ob__;
// 3. 处理新增元素(需要响应式化)
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args; // 新增的元素
break;
case 'splice':
inserted = args.slice(2); // splice(索引, 删除数, 新增元素...)
break;
}
// 4. 对新增元素进行响应式处理
if (inserted) {
ob.observeArray(inserted);
}
// 5. 通知依赖更新!
ob.dep.notify();
// 6. 返回原生方法结果
return result;
});
});
四、如何让数组使用增强方法?
✅ 在 Observer
中"偷天换日"
js
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
// 关键:修改数组的原型
if (Array.isArray(value)) {
// value.__proto__ = arrayMethods
value.__proto__ = arrayMethods;
// 或对非浏览器环境使用方法拷贝
// def(value, method, arrayMethods[method])
} else {
this.walk(value);
}
}
}
💥 这样一来,所有响应式数组调用
push
等方法时,实际执行的是增强版方法。
五、7 个被劫持的方法详解
方法 | 是否新增元素 | 是否触发更新 |
---|---|---|
push(...items) |
✅ | ✅ |
pop() |
❌ | ✅(长度变) |
shift() |
❌ | ✅(长度变) |
unshift(...items) |
✅ | ✅ |
splice(start, deleteCount, ...items) |
✅ | ✅ |
sort() |
❌ | ✅(顺序变) |
reverse() |
❌ | ✅(顺序变) |
⚠️ 注意:
pop
、shift
虽不新增,但改变了数组,所以也要notify
。
六、实战演示
📌 场景:动态添加列表项
vue
<template>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
<button @click="addItem">添加</button>
</template>
<script>
export default {
data() {
return {
items: [{ id: 1, name: 'A' }]
};
},
methods: {
addItem() {
// 调用被劫持的 push
this.items.push({ id: 2, name: 'B' });
// 1. 执行原生 push
// 2. observeArray([{ id: 2, name: 'B' }]) → 响应式化
// 3. dep.notify() → 视图更新
}
}
}
</script>
七、Vue 3 的革命:Proxy
彻底解决
✅ Vue 3 响应式原理
js
const arr = reactive(['a', 'b']);
arr[2] = 'c'; // ✅ Proxy 捕获 set
arr.length = 0; // ✅ 捕获 length 变化
arr.push('d'); // ✅ 原生方法调用,但 Proxy 仍能感知数组变化
💥 Vue 3 不再需要劫持数组方法 ,
Proxy
天然支持所有变化监听。
八、其他数组操作的注意事项
❌ Vue 无法检测的操作
js
// 1. 索引直接赋值
this.items[0] = { ... }; // ❌
// 2. 修改数组长度
this.items.length = 0; // ❌
✅ 解决方案(Vue 2)
js
// 使用 $set
this.$set(this.items, 0, { ... });
// 使用 splice
this.items.splice(0, 1, { ... });
// 清空数组
this.items.splice(0);
// 或
this.items = [];
💡 结语
"Vue 的数组响应式,是工程智慧的典范。"
机制 | Vue 2 | Vue 3 |
---|---|---|
核心技术 | 方法劫持 + __proto__ |
Proxy |
劫持方法 | 7 个变异方法 | 无需劫持 |
监听能力 | 部分支持 | 完全支持 |
方法 | 增强逻辑 |
---|---|
push |
响应式化新元素 + 通知更新 |
splice |
响应式化新增项 + 通知更新 |
sort |
直接触发更新(顺序变) |
掌握这一机制,你就能:
✅ 理解 push
为何能更新视图;
✅ 避开 this.items[0] = value
的陷阱;
✅ 在 Vue 2 和 Vue 3 间平滑迁移。