Vue2.0为什么不能检查数组的变化?又该如何解决?
前言
Vue 2 的响应式实现存在两个常见边界:
- 对象初始化后直接新增或删除属性,无法被自动检测。
- 直接通过索引修改数组、直接修改数组
length,无法触发响应式更新。
Vue 2 已停止维护,下面的写法主要用于维护旧项目;新项目应优先使用 Vue 3。
分析
Vue 2 在初始化时遍历已有属性,通过 Object.defineProperty() 把它们转换为 getter/setter。事后直接增加的对象键并没有对应 setter,因此不能自动通知更新。
数组并非“完全不能检测”。Vue 2 专门包装了七个变异方法:push、pop、shift、unshift、splice、sort、reverse。这些方法能够通知依赖;真正的边界是直接索引赋值和直接修改 length。
思考:索引赋值真的不能被原生 API 监听吗?
下面保留原文的教学实验:遍历数组当前已有索引,用 Object.defineProperty() 安装访问器。
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log(`get key: ${key} value: ${value}`)
return value
},
set(newValue) {
console.log(`set key: ${key} value: ${newValue}`)
value = newValue
}
})
}
function observe(data) {
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key])
})
}
const arr = [1, 2, 3]
observe(arr)
arr[1] = 20

实验中 arr[1] 会触发 setter,说明原生 API 能观察当时已经存在且已转换的索引。但它不能证明 Vue 2 的 Observer 就这样处理数组,也不能覆盖新增索引、稀疏数组与 length。Vue 2 实际选择了“观察已有对象属性 + 包装数组变异方法”的实现。
后续理解
原文后续讨论指出数组可能很大,并试图把这一行为单纯归因于每次变更都要遍历带来的性能损耗:

更准确的结论是:这是 Vue 2 基于 Object.defineProperty() 的实现边界与设计取舍,不是“数组每次变化都必须重新遍历,因此无法检测”。初始化时已有的对象嵌套属性会被递归观察,数组的七个变异方法也会响应;直接索引、length、对象键新增和删除才需要专门 API。
Vue 2 历史解决方案
数组
- 使用
Vue.set/this.$set:
this.$set(this.dataArr, 0, { data: '修改第一个元素' })
如果 dataArr 和 originArr 指向同一个数组,两边都会变化,这是共享引用造成的,不是 $set 的“深度修改”特性。
- 使用
splice完成增、删、改或截断:
this.items.splice(index, 1, value)
this.items.splice(newLength)
- 用新数组替换旧数组:
const next = [...this.targetArr]
next[0] = { data: 'test' }
this.targetArr = next
展开运算符只浅拷贝数组容器,元素对象的嵌套引用仍然共享。
对象
使用 Vue.set / $set 新增属性,使用 Vue.delete / $delete 删除属性:
this.$set(this.blog, 'title', 'Vue 2')
this.$delete(this.blog, 'draft')
深度监听用于观察已有嵌套数据,以及通过响应式 API 完成的新增/删除:
this.$watch('blog', this.getCatalog, {
deep: true,
immediate: true
})
deep: true 需要遍历嵌套结构,不能把普通的 obj.newKey = value 自动变成响应式。就地修改对象或数组时,回调中的 newValue 与 oldValue 还可能是同一个引用,它不是前后快照。
只监听某个键时可以写:
watch: {
'obj.name'(currentValue, oldValue) {
console.log(currentValue, oldValue)
}
}
当前 Vue 3 说法
Vue 3 的 reactive() 使用 Proxy,能够拦截对象键新增/删除、数组索引和 length。ref() 可以持有原始值;持有对象时会进行深层响应式转换。
import { reactive, ref, watchEffect } from 'vue'
const items = reactive(['a', 'b'])
const selected = ref(0)
watchEffect(() => {
console.log(items[selected.value], items.length)
})
items[1] = 'Vue 3'
items.length = 1
selected.value = 0
这是可运行的依赖追踪示例;与只打印赋值前后值不同,watchEffect 会在读取的响应式依赖变化后重新执行。
官方参考
- Vue 2:深入响应式原理(历史)
- Vue 2:列表渲染与数组变更检测(历史)
- Vue 2 API:Vue.set / Vue.delete / vm.$watch(历史)
- Vue 3:响应式基础
- Vue 3:响应式核心 API
作者:小只前端攻城狮
原文链接:https://juejin.cn/post/6970752937047883789
来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。