概述
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 牺牲了一点开发体验。
最后
以上就是稳重墨镜为你收集整理的浅探 Vue 2为什么没有数组下标响应式的全部内容,希望文章能够帮你解决浅探 Vue 2为什么没有数组下标响应式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复