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(以下只贴出关键代码部分,其他部分代码一致):
- 使用修改能响应下标触发页面更新的 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>
- 使用原版 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 牺牲了一点开发体验。