记一次思否问答的问题思考:Vue为什么不能检测数组变动

112

问题来源:https://segmentfault.com/q/10...

问题描述:Vue检测数据的变动是通过Object.defineProperty实现的,所以无法监听数组的添加操作是可以理解的,因为是在构造函数中就已经为所有属性做了这个检测绑定操作。

但是官方的原文:由于 JavaScript 的限制, Vue 不能检测以下变动的数组:

当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如: vm.items.length = newLength

这句话是什么意思?我测试了下Object.defineProperty是可以通过索引属性来设置属性的访问器属性的,那为何做不了监听?

有些论坛上的人说因为数组长度是可变的,即使长度为5,但是未必有索引4,我就想问问这个答案哪里来的,修改length,新增的元素会被添加到最后,它的值为undefined,通过索引一样可以获取他们的值,怎么就叫做“未必有索引4”了呢?

既然知道数组的长度为何不能遍历所有元素并通过索引这个属性全部添加set和get不就可以同时更新视图了吗?

如果非要说的话,考虑到性能的问题,假设元素内容只有4个有意义的值,但是长度确实1000,我们不可能为1000个元素做检测操作。但是官方说的由于JS限制,我想知道这个限制是什么内容?各位大大帮我解决下这个问题,感谢万分



面对这个问题,我想说的是,首先,长度为1000,但只有4个元素的数组并不一定会影响性能,因为js中对数据的遍历除了for循环还有forEach、map、filter、some等,除了for循环外(for,for...of),其他的遍历都是对键值的遍历,也就是除了那四个元素外的空位并不会进行遍历(执行回调),所以也就不会造成性能损耗,因为循环体中没有操作的话,所带来的性能影响可以忽略不计,下面是长度为10000,但只有两个元素的数组分别使用for及forEach遍历的结果:

var arr = [1]; arr[10000] = 1
function a(){
    console.time()
    for(var i = 0;i<arr.length;i++)console.log(1)
    console.timeEnd()
}
a(); //default: 567.1669921875ms
a(); //default: 566.2451171875ms

function b(){
    console.time()
    arr.forEach(item=>{console.log(2)})
    console.timeEnd()
}
b(); //default: 0.81982421875ms
b(); //default: 0.434814453125ms

可以看到结果非常明显,不过,如果for循环中不做操作的话两者速度差不多

其次,我要说的是,我也不知道这个限制是什么      (⇀‸↼‶)      ╮( •́ω•̀ )╭

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。数组的索引也是属性,所以我们是可以监听到数组元素的变化的

var arr = [1,2,3,4]
arr.forEach((item,index)=>{
    Object.defineProperty(arr,index,{
        set:function(val){
            console.log('set')
            item = val
        },
        get:function(val){
            console.log('get')
            return item
        }
    })
})
arr[1]; // get  2
arr[1] = 1; // set  1

但是我们新增一个元素,就不会触发监听事件,因为这个新属性我们并没有监听,删除一个属性也是。

再回到题主的问题,既然数组是可以被监听的,那为什么vue不能检测vm.items[indexOfItem] = newValue导致的数组元素改变呢,哪怕这个下标所对应的元素是存在的,且被监听了的?

为了搞清楚这个问题,我用vue的源码测试了下,下面是vue对数据监测的源码:
Observer

可以看到,当数据是数组时,会停止对数据属性的监测,我们修改一下源码:
修改Observer

使数据为数组时,依然监测其属性,然后在defineReactive函数中的get,set打印一些东西,方便我们知道调用了get以及set。这里加了个简单判断,只看数组元素的get,set
修改defineReactive

然后写了一个简单案例,主要测试使用vm.items[indexOfItem] = newValue改变数组元素能不能被监测到,并响应式的渲染页面
简单案例

运行页面
数组测试

可以看到,运行了6次get,我们数组长度为3,也就是说数组被遍历了两遍。两遍不多,页面渲染一次,可能多次触发一个数据的监听事件,哪怕这个数据只用了一次,具体的需要看尤大代码怎么写的。就拿这个来说,当监听的数据为数组时,会运行dependArray函数(代码在上面图中get的实现里),这个函数里对数组进行了遍历取值操作,所以会多3遍get,这里主要是vue对data中arr数组的监听触发了dependArray函数。

当我们点击其中一个元素的时候,比如我点击的是3
点击3

可以看到会先运行一次set,然后数据更新,重新渲染页面,数组又是被遍历了两遍。

但是!!!数组确实变成响应式的了,也就是说js语法功能并不会限制数组的监测。

这里我们是用长度为3的数组测试的,当我把数组长度增加到9时
新数组测试

可以看到,运行了18次get,数组还是被遍历了两遍,点击某个元素同理,渲染的时候也是被遍历两次。
新数组测试

有了上面的实验,我的结论是数组在vue中是可以实现响应式更新的,但是不明白尤大是出于什么考虑,没有加入这一功能,希望有知道的大佬们不吝赐教


2018-07-27补充

github上提问了尤大
github提问

你可能感兴趣的

34 条评论
水晶之夜 · 2018年07月28日
关于问题部分

大家经常把vue和angularjs对标,那么angularjs对于这种情况是如何应对的?个人测试angularjs可以监听数组元素修改,但是数组长度修改会报错(报错链接).即便说所谓性能问题,但是为什么就直接不提供了,好的解决方式不应该是提供方法但说明性能风险,让用户知道?

关于个人吐槽部分(以下仅为个人观点,如您不喜欢,请跳过此段)

首先题主的"尤大"其实让我有点反感。反正我遇到的XX大印象都不怎么好(V2EX有Livid大,可惜V2EX是个私人论坛)。其次,关于问题部分,题主查询官方文档说

由于 JavaScript 的限制, Vue 不能检测以下变动的数组:

当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如: vm.items.length = newLength

当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如: vm.items.length = newLength

然后,当答主去github向"尤大"提问时,"尤大"的答案是

就是因为性能原因

尤大反应一是:

要求写的很清楚,不要用issue问问题 (私以为数组变动问题难道不算一个BUG,或者说有待提高的功能,所以"尤大"简单归类为"问问题")

反映二是

破例回答一下你的问题 (看来"尤大"真的很忙,这些"简单问题"还是不要麻烦"尤大"回答了,你们不知道自己解决吗?另外我从评论里才知道,原来"尤大"知乎帐号禁止私信的,看来确实很忙,没时间解决你们这些问题啊)

反映三是当答主提出 具体性能上有什么问题的时,尤大的反映是

性能代价和获得的用户体验不成正比 (emmmm,言简意赅,言简意赅)

我本人不是职业前端,但是经常会独立开发一些个人网站。在前端选型的时候,最开始用的vue,但是有一个问题没解决(简单讲是页面弹出框没有正常弹出,js没有报错)。然后我换了angularjs,同样的功能(vue和angularjs的的api部分不一样,但功能很多是相似的),angularjs顺利弹出。

再加上之前有看过网上关于angularjs和vue比较的争论,我看到有人评论说vue是在做商业,angularjs是在做技术。 "尤大"自己在知乎上也写过对比的专栏,但是文风其实我很不喜欢。

总结

数组变动部分,angularjs测试是可以监听数组元素变化的,但是修改数组长度js会报错报错链接
另外从"尤大"自相矛盾的讲法(官方文档写js的限制,github回答时说就是因为性能原因),以及angularjs可以实现数组元素监听功能,说明vue其实也是做到监听数组的。关于所谓性能原因,期盼"尤大"能够给出更详细的数据部分,毕竟数据会说话。

+5 回复

0

"尤大"毕竟不大,一个自尊心强的年轻人。

大boss · 7月4日
0

MobX 4同样的实现原理,但是就没有所谓js的限制

大boss · 7月4日
0

"性能代价和获得的用户体验不成正比 ",说的很清楚啊。框架设计就是在无数个有关性能和便捷问题上做取舍,这个细节确实对开发不够友好,但因此vue的整体性能可能提升了一丢丢,起码超过了angular,这个结果作为开发者你不满意吗?不要用“完美”去要求一个框架,不是angular能做到vue就一定要做到。

雅X共赏 · 7月11日
mafeifan · 2018年07月27日

楼主打破砂锅问到底的精神值得赞扬。
另外吐槽下sf的app设计,评论按钮不够醒目

+2 回复

SHERlocked93 · 2018年07月27日

这样做早有人讨论,你是把数组下标当做对象的属性进行defineReactive的,如果数组只有10个属性,这样做没问题,如果数组长度为1000呢,挨个defineReactive么?性能消耗太大,vue的做法是修改原生操作数组的方法,并且跟用户约定修改数组要用这些方法去操作

+1 回复

0

1000个属性真不多,对象属性也是属性,4000多条数据的数组,每条数据是个对象,里面有60多个属性,vue都可以很好的实现监测

hfhan 作者 · 2018年07月27日
0

@hfhan 可以去vue的github上提一下issue

SHERlocked93 · 2018年07月27日
0

作者的看法我是支持的。js的数组本质就是一种键值是0.1.2.3的特殊对象。所以"尤大"不大

大boss · 7月4日
liximomo · 2018年07月27日
因为js中对数据的遍历除了for循环还有forEach、map、filter、some等,除了for循环外(for,for...of),其他的遍历都是对键值的遍历,也就是除了那四个元素外的空位并不会进行遍历,所以也就不会造成性能损耗,

这句话不对, 不是不会遍历, 是不会对不存在的值执行回调.

var arr = [1]; arr[10000] = 1
arr.forEach(item=>{console.log(2)})

实际上还是遍历了 10000 次, 只是回调函数只执行了2次.

var arr = [1]; arr[100000] = 1
function a(){
    console.time()
    arr.forEach(item=>{})
    console.timeEnd()
}
a(); //default: 2.5810546875ms
a(); //default: 2.02099609375ms

var arr1 = new Array(100000).fill(1);
function b(){
    console.time()
    arr1.forEach(item=>{})
    console.timeEnd()
}
b(); //default: 2.355224609375ms
b(); //default: 1.94189453125ms

两个要花费同样的时间来遍历. 确确实实存在遍历的性能问题.

+1 回复

0

多谢提醒,可能我记错了,忘了哪种遍历和for...in一样,是对键值的遍历,可能就只有for...in~

hfhan 作者 · 2018年07月27日
0

'实际上还是遍历了 10000 次, 只是回调函数只执行了2次'。
大佬 怎么知道实际上遍历10000次的 ,还是实际执行2次的 ?

彼岸花开 · 2018年07月27日
0

@彼岸花开 看我最后一个例子, 时间是一样的

liximomo · 2018年07月27日
breathlessway · 2018年07月27日

mobx 好像就是一次创建 99999 长度的数组

+1 回复

牧毅 · 2018年07月27日

我觉得你应该在知乎上@evanyou,尤大似乎不来SF

回复

0

尤大禁止了死心,所以在知乎上提问,并邀请了尤大

hfhan 作者 · 2018年07月27日
牧毅 · 2018年07月27日

如果要高效地监听数组的改动,其实可以考虑使用 Proxy, 不过问题是IE不支持。

回复

改名字很伤神 · 2018年07月27日

用上面的例子,如果是arr[10] = 10呢。

Array.length 属性的属性特性:
writable true
enumerable false
configurable false

回复

0

arr[10] = 10相当于是增加属性,这个无论是对象还是数组都不能监测到

hfhan 作者 · 2018年07月27日
0

@hfhanarr.length是原有的属性吧,arr.length=10呢?

改名字很伤神 · 2018年07月27日
0

@改名字很伤神 数组的length不能重新定义,所以不能监听。其实我想问的以及实验的是,在数组已有元素中(非对象元素,对象元素已经实现了监听),更改某一元素的值,是可以实现响应式的,为什么vue中没有加入这一功能

hfhan 作者 · 2018年07月27日
Yooloo · 2018年07月27日

你第一个例子举得严重有问题...
我改了一下你的例子代码:
var arr = [1]; arr[10000] = 1;
var len = arr.length;
function a(){

for(var i = 0;i<len ;i++){
}

}
console.time()
a();
a();
console.timeEnd()

function b(){

console.time()
arr.forEach(item=>{})
console.timeEnd()

}
b();
b();
可以看到输出结果差距不大。在这里forEach执行时间要短一些的原因在于arr里面实际只有两个项,所以迭代了两次。而for循环则迭代了arr.length次。改for循环为for in 循环就只会迭代两次,for in 循环的执行时间则明显少于forEach循环。

回复

mafeifan · 2018年07月27日

楼主的打破砂锅问到底的精神值得赞扬
另外吐槽下sf的app。评论按钮不够醒目。

回复

幻白一驹 · 8月5日

你了解什么叫空间和时间优化吗,如果数组里加入监听,试想一下操作一个以万为单位的大型数组,得付出多少时间和空间

回复

载入中...