8.1 数组响应化的特殊挑战
8.1.1 数组操作类型分析
操作类型 | 示例方法 | 响应化难度 |
---|---|---|
变异方法 | push/pop/shift/unshift | 可拦截 |
替换方法 | splice/sort/reverse | 可拦截 |
索引操作 | arr[0] = newVal | 无法检测 |
长度修改 | arr.length = 0 | 无法检测 |
8.1.2 设计约束
- 兼容性:需要支持ES5环境
- 性能:不能对大型数组造成明显负担
- 透明性:保持数组原生方法的行为不变
8.2 数组方法拦截实现
8.2.1 原型链重写方案
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto) // 创建纯净副本
const methodsToPatch = [
'push', 'pop', 'shift', 'unshift',
'splice', 'sort', 'reverse'
]
methodsToPatch.forEach(method => {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
// 处理新增元素
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 响应化新增元素
if (inserted) ob.observeArray(inserted)
// 通知变更
ob.dep.notify()
return result
})
})
关键实现解析:
- 原型链继承:
arrayMethods
继承原生数组方法 - 方法重写:在调用原生方法后触发通知
- 新增元素处理:特别处理可能添加新元素的方法
8.3 数组的响应化初始化
8.3.1 Observer类增强
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this) // 标记已观察
if (Array.isArray(value)) {
// 数组的特殊处理
this.observeArray(value)
protoAugment(value, arrayMethods) // 修改原型链
} else {
this.walk(value)
}
}
observeArray(items) {
for (let i = 0; i < items.length; i++) {
observe(items[i]) // 递归观察数组元素
}
}
}
function protoAugment(target, src) {
target.__proto__ = src // 直接修改原型链
}
原型链修改示意图:
观测数组实例的原型链:
arr → Vue包装的原型对象 → 原生Array.prototype
8.4 Vue.set/Vue.delete 实现
8.4.1 Vue.set 源码解析
function set(target, key, val) {
// 处理数组情况
if (Array.isArray(target)) {
key = Math.max(key, 0)
target.splice(key, 1, val) // 利用splice触发通知
return val
}
// 处理对象情况
if (key in target) {
target[key] = val
return val
}
const ob = target.__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val) // 响应化新属性
ob.dep.notify()
return val
}
8.4.2 Vue.delete 源码解析
function del(target, key) {
if (Array.isArray(target)) {
target.splice(key, 1) // 数组使用splice
return
}
if (!hasOwn(target, key)) return
delete target[key]
const ob = target.__ob__
if (!ob) return
ob.dep.notify()
}
统一处理策略:
- 数组使用splice方法触发更新
- 对象直接删除属性并通知
- 确保删除操作触发视图更新
8.5 数组响应化的局限与解决方案
8.5.1 无法检测的情况
// 情况1:直接索引设置
vm.items[0] = newValue // 不会触发视图更新
// 情况2:修改数组长度
vm.items.length = 0 // 不会触发视图更新
8.5.2 解决方案比较
方法 | 示例 | 原理 |
---|---|---|
Vue.set() | Vue.set(arr, 0, newVal) | 调用splice |
Array.prototype.splice | arr.splice(0, 1, newVal) | 触发重写的方法 |
整体替换 | arr = arr.map(…) | 引用变更触发响应 |
8.6 性能优化策略
8.6.1 大型数组处理技巧
// 冻结大型数组(避免响应式开销)
const largeArray = Object.freeze([/* 大量数据 */])
// 分块处理
function chunkUpdate(arr, newData) {
arr.splice(0) // 清空原数组
newData.forEach((item, i) => {
if (i % 100 === 0) {
// 分批次触发更新
Vue.nextTick(() => arr.push(...newData.slice(i, i+100)))
}
})
}
8.6.2 响应式标记机制
function observe(value) {
if (!isObject(value)) return
// 跳过被标记为非响应的对象
if (value.__ob__ && value.__ob__.skip) return
// 正常响应化处理...
}
本章重点总结:
- 方法拦截:通过原型链重写实现数组方法监听
- 边界处理:Vue.set/Vue.delete 的补充机制
- 性能权衡:数组响应化的设计取舍
- 最佳实践:安全操作数组的正确方式
深度实践建议:
- 对比直接修改索引和使用Vue.set的区别
- 实现自定义数组方法拦截器
- 性能测试大型数组的不同操作方式
// 性能测试示例
const vm = new Vue({
data: { items: Array(10000).fill(0) }
})
// 测试不同修改方式
console.time('直接赋值')
vm.items[5000] = 1 // 不会触发更新
console.timeEnd('直接赋值')
console.time('Vue.set方式')
Vue.set(vm.items, 5000, 1) // 触发更新
console.timeEnd('Vue.set方式')