Vue2 数组变化检测问题

Vue2 无法检测数组索引变化的原因及解决方案

问题

Vue2 无法检测数组的某些变化操作:

  • 无法检测数组/对象的新增属性
  • 无法检测通过索引改变数组的操作,如 vm.items[0] = newValue

解答

为什么无法检测

Vue2 使用 Object.defineProperty 实现响应式。对于数组新增元素,因为在初始化时已经完成了属性绑定,所以无法监听后续添加的元素。

索引操作真的无法监听吗?

实际上 Object.defineProperty 可以监听数组索引变化:

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function defineGet() {
      console.log(`get key: ${key} value: ${value}`)
      return value
    },
    set: function defineSet(newVal) {
      console.log(`set key: ${key} value: ${newVal}`)
      value = newVal
    }
  })
}

function observe(data) {
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key])
  })
}

let arr = [1, 2, 3]
observe(arr)
arr[1] = 5  // 触发 set,说明可以监听

测试结果表明,通过索引修改数组确实会触发 set。Vue2 不实现这个功能是出于性能考虑:数组可能包含成千上万个元素,每次变更都遍历数组会带来巨大的性能损耗。

Vue3 使用 Proxy 后已解决这个问题。

解决方案

数组操作:

  1. 使用 $set 方法
this.$set(this.dataArr, 0, {data: '修改第一个元素'})
// 注意:这会同时修改源数组
  1. 使用 splice 方法
// splice 已被 Vue 重写,具有响应式
this.dataArr.splice(0, 1, {data: 'new value'})
  1. 使用临时变量
let tempArr = [...this.targetArr]
tempArr[0] = {data: 'test'}
this.targetArr = tempArr

对象操作:

  1. 使用 $set 新增或修改属性
this.$set(this.obj, 'newKey', 'value')
  1. 深度监听(只能监听属性值变化,无法监听新增/删除)
this.$watch('blog', this.getCatalog, {
  deep: true
})
  1. 监听具体属性
watch: {
  'obj.name'(curVal, oldVal) {
    // 处理逻辑
  }
}

关键点

  • Vue2 不监听数组索引变化是性能权衡,而非技术限制
  • 数组使用变异方法(push、pop、shift、unshift、splice、sort、reverse)或 $set 可触发响应式
  • 对象新增属性需要使用 $set 方法
  • Vue3 的 Proxy 方案已解决这些限制