Vue2 重写了数组的 7 个变更方法(原理)
由于 Object.defineProperty 无法监听数组索引变化,Vue2 重写了数组的 7 个变更方法。调用这些方法时,会手动触发视图更新。
核心问题拆解
- 为什么 Object.defineProperty 无法监听数组索引变化?
const arr = [1, 2, 3];
// 尝试劫持数组索引
Object.defineProperty(arr, '0', {get() { console.log('get 0') },set() { console.log('set 0') }
});
-
实际效果:
-
能监听 arr[0] 的读写(但仅限于已存在的索引)
-
致命缺陷:
-
无法检测 arr[10] = 100(越界赋值新索引)
-
无法检测 arr.length = 0(修改长度)
-
性能灾难:对数千个索引劫持会造成严重性能损耗
-
-
- Vue2 的解决方案:重写数组方法
// 1. 备份数组原型
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);// 2. 重写7个变更方法
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
methodsToPatch.forEach(method => {const original = arrayProto[method];Object.defineProperty(arrayMethods, method, {value: function mutator(...args) {const result = original.apply(this, args); // 执行原生方法const ob = this.__ob__; // 获取关联的Observer实例let inserted;// 特殊处理新增元素的方法(如push/unshift/splice)if (method === 'push' || method === 'unshift') {inserted = args;} else if (method === 'splice') {inserted = args.slice(2); // splice(1,1,item1,item2)新增的元素}// 对新增元素做响应式处理if (inserted) ob.observeArray(inserted);ob.dep.notify(); // 🔑 手动触发更新return result;}});
});// 3. 替换目标数组的原型
arr.__proto__ = arrayMethods; // 让数组调用重写后的方法
关键流程解析
当调用 arr.push(100) 时:
- 执行重写后的 push 方法
arr.push(100)
↓
mutator([100]) // 进入重写方法
-
完成三件事:
-
用原生 Array.prototype.push 添加元素
-
对新元素 100 做响应式处理(若它是对象)
-
手动调用 ob.dep.notify() 通知所有 Watcher 更新
-
对比直接修改索引:
// 直接修改索引(无法触发响应式)
arr[0] = 999; // 不会更新视图!// 必须用 Vue.set 或重写方法
Vue.set(arr, 0, 999); // 或 arr.splice(0, 1, 999)
设计原理深度剖析
场景 | 检测方式 | 原理 |
---|---|---|
arr[0] = 1 | 无法检测 | Object.defineProperty 只能劫持已存在的索引 |
arr.length = 0 | 无法检测 | 数组长度变化无法通过属性劫持捕获 |
arr.push(100) | 通过重写方法检测 | 在方法执行后手动触发更新 |
arr.splice(1, 0, {x:1}) | 通过重写方法检测 | 1. 执行原生方法 2. 对新对象 {x:1} 做响应式处理 3. 触发更新 |
关键结论
-
重写方法 ≠ 监听索引:Vue2 从未尝试劫持数组索引,而是通过拦截方法调用实现更新
-
手动更新是核心:在重写方法中主动调用 dep.notify() 是触发视图更新的关键
-
一致性代价:开发者必须使用重写方法或 Vue.set 操作数组,否则破坏响应式
这种设计在性能与功能间取得了平衡,是 Vue2 响应式系统的重要智慧。