浅探 Vue 为什么不增加数组下标响应式——为什么不能检测到数组元素直接赋值

Vue 的双向数据绑定,使得修改数据后,视图就会跟着发生更新,比如对数组进行增加元素、切割等操作。然而直接通过下标修改数组内容后,视图却不发生变化。那么,在保留原有的数组响应方式下,为什么 Vue 不增加对数组下标的响应式监听呢?

arr[index] = val 不是响应式的

在 Vue 官网的 列表渲染 — Vue.js 中,有强调 Vue 不能 直接检测通过数组下标改变值的变化,需要通过 数组更新检测 来实现。

<template>
  <div>
    <span v-for="i in arr">{{ i }}</span>
    <button @click="updateIndex">改变下标对应的值</button>
    <span v-for="key in Object.keys(obj)">{{ obj[key] }}</span>
    <button @click="updateKey">改变key对应的值</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      arr: [ 1, 2, 3, 4 ],
      obj: { a: 1, b: 2, c: 3, d: 4 }
    }
  },
  methods: {
    updateIndex() {
      this.arr[0]++                // 对数组这样的操作不会引起视图的更新
      // this.arr.splice(0, 0)     // 需要调用数组的方法,才能使视图更新
    },
    updateKey() {
      this.obj['a']++    // 但对对象这样会引起视图更新
    }
  }
}
</script>

 

 

 

从源码看 Vue 中数组的 Observer 实现

在 Vue 2.6.10 中,可以看到 Observer (/src/core/observer/index.js) 的实现方式:

export class Observer {
  // ......
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 这里对数组进行单独处理
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // 对对象遍历所有键值
      this.walk(value)
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

可以看到 vue 对对象是采取 Object.keys然后 defineReactive 所有键值,而对数组并没这样做,而是只 observe 了每个元素的值,数组的下标因为没有被监听,所以直接通过下标修改值是不会更新视图的。

而数组方法能够响应式,是因为 Vue 对数组的方法进行了 def 操作 (/src/core/observer/array.js)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // cache original 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)
    // notify change
    ob.dep.notify()
    return result
  })
})

并非不能实现下标响应式

但数组也是对象的一种,它的下标就是它的键,只是平常使用时,数组的键数量往往比对象的键数量大的多。所以原则上它也是可以使用对象的处理方式。通过修改 源码 后引入后查看效果:

export class Observer {
  // ....
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
      this.walk(value)    // 保留原有的数组监听方式下,增加对下标的监听响应
    } else {
      this.walk(value)
    }
  }
  // ......
}

视图代码还是和上面的一样,点击按钮可以看到视图会实时更新:

 

实验探测数组下标响应式对性能的影响

通过上面的修改,可以知道 Vue 其实是可以监听数组下标的。但为什么 Vue 不采取,且说是“JavaScript的限制”呢?在 github issue#8562 中,Vue.js 作者尤雨溪解释是因为性能问题。

为了验证数组下标响应式对性能的影响,我做了以下实验实现相同的效果,分别设置循环次数 TIMES 为 1000,10000,100000(以下只贴出关键代码部分,其他部分代码一致):

  1. 使用修改能响应下标触发页面更新的 Vue ,通过数组下标修改值 TIMES 次
<template>
  <div>
    <span v-for="i in arr">{{ i }}</span>
    <button @click="updateIndex">改变下标对应的值</button>
  </div>
</template>
<script>
// import modified vue
export default {
  data() {
    return {
      arr: new Array(100).fill(0)
    }
  },
  methods: {
    updateIndex() {
      console.time('updateIndex')
      for (let i = 0; i < TIMES; i++) {
        this.arr[0]++
      }
      console.timeEnd('updateIndex')
    }
  }
}
</script>
  1. 使用原版 vue 通过数组下标修改值 TIMES 次,并通过 splice 方法触发视图更新 
<template><!-- 和上面一样 --></template>
<script>
// import origin vue
export default {
  data() { /* 和上面一样 */ },
  methods: {
    updateIndex() {
      console.time('updateIndex')
      for (let i = 0; i < TIMES; i++) {
        this.arr[0]++
      }
      this.arr.splice(0, 0)    // 通过 splice 实现视图更新
      console.timeEnd('updateIndex')
    }
  }
}
</script>

每个实验不同 TIMES 都重复10次,取平均值,实验数据如下:

 

增加数组下标响应式对性能会有影响

通过上面的实验,可见在循环次数较少的时候,增加下标响应式似乎没有多大影响,但随循环次数增加,带来的性能损耗将快速增加。如果想要实现直接修改下标对应的内容来自动更新视图,对性能会有一些影响。因此对于数组的更新,最好还是通过数组更新检测来实现。

在选择 TIMES 取值的时候,也发现需要到 10000 级别才会体现出较明显的差距。但一般情况下,我们并不会执行像上面一样庞大的操作,也许仅仅只是改变一个值而已,实现下标响应式消耗的时间和普通的方式几乎一样,或许在这方面 vue 牺牲了一点开发体验。

 转自:https://blog.csdn.net/dobility/article/details/97261478

posted @ 2021-02-03 22:26  vickylinj  阅读(1185)  评论(0编辑  收藏  举报