解决Vue修改数组/字典中的元素,却无法检测到数据变化的问题

一 背景

我在做毕设的项目中,写前端逻辑代码时遇到了一个问题:通过用户click触发一个函数,在函数中根据选中的index,动态修改Map容器中的对应下标的值,以此用不同的class来凸显用户的选择项。实现目标效果图如下图(解决了本篇所要解决的问题后):

{width=”50%”}


二 实现思路

先说下实现这种样子的思路,首先我在设计后端API的时候,要注意返回的Json数据格式,每一个商品都会有多个sku属性,每个sku属性都会对应多个sku属性值,因此这样关系很清晰,通过1:N外键关系就可以表示。格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
...
"sku_props": [
{
"id": 8,
"sku_values": [
{
"id": 19,
"value": "黄色",
"prop": 8
},
{
"id": 20,
"value": "白色",
"prop": 8
},
{
"id": 21,
"value": "黑色",
"prop": 8
}
],
"name": "颜色",
"commodity": 32
},
{
"id": 10,
"sku_values": [
{
"id": 26,
"value": "小型",
"prop": 10
},
{
"id": 27,
"value": "中型",
"prop": 10
},
{
"id": 28,
"value": "大型",
"prop": 10
},
{
"id": 29,
"value": "特大型",
"prop": 10
}
],
"name": "大小",
"commodity": 32
},
{
"id": 12,
"sku_values": [
{
"id": 36,
"value": "小型",
"prop": 12
},
{
"id": 37,
"value": "中型",
"prop": 12
},
{
"id": 38,
"value": "大型",
"prop": 12
},
{
"id": 39,
"value": "特大型",
"prop": 12
}
],
"name": "大小",
"commodity": 32
},
{
"id": 13,
"sku_values": [
{
"id": 40,
"value": "微辣",
"prop": 13
},
{
"id": 41,
"value": "中辣",
"prop": 13
},
{
"id": 42,
"value": "特辣",
"prop": 13
},
{
"id": 43,
"value": "变态辣",
"prop": 13
}
],
"name": "辣度",
"commodity": 32
},
{
"id": 14,
"sku_values": [
{
"id": 48,
"value": "12GB",
"prop": 14
}
],
"name": "内存",
"commodity": 32
}
],
...

这样,我在vue中通过v-for两次循环遍历即可表示出多个sku属性,每个属性下对应多个sku属性值。


那么接下来的需求就是用户选择每个sku属性下的属性值,我需要记录下来并高亮标注显示给用户,因此我是采用字典,姑且取个名字,choiceMap,其中key为每个属性的名字,value为选中的值的下标。当用户选择了某项时,choiceMap中对应key的value的下标变化成选中的属性值的下标,然后在template中使用,:class,通过表达式(判断choiceMap[key] === index)是否为真,来增加/消除颜色高亮风格的class。对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div v-for="(values, index) in propsValues" :key="index">
<div class="prop-name">{{ values.name }}</div>
<van-button
v-for="(value, index) in values.sku_values"
:key="index"
:class="[
'value-btn',
{ 'value-btn-choice': index === choiceMap[values.name] },
]"
@click="choiceValue(index, values)">
{{ value.value }}
</van-button>
<van-divider></van-divider>
</div>

说明:点击某个属性值,调用choiceValue(index, values)来实现高亮显示。我一开始使用的方式是:

1
2
3
4
choiceValue(index, values) {
this.choiceMap[values.name] = index;
console.log(this.choiceMap)
},

通过控制台打印发现,数据确实更新了,但是view视图却没有更新,后来我测试了一下数组,发现同样的问题。由于鄙人前端没有深入,因此只得百度搜索解决方案,幸运的是很快找到了解决方法,在仔细阅读别人的博客后,发现了问题所在—–Vue不能检测到对象的添加或者删除。然而Vue在初始化实例时就对属性执行了setter/getter转化过程,所以属性必须开始就在对象上,这样才能让Vue转化它。 这句话什么意思呢?别急,我用几个解释以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 假设我在实例初始化时在data域中设置元素并进行初始化
data() {
return {
testMap:{
'name':'syz',
'age':22
}
};
// 测试Dom
<van-button @click="test"></van-button>

// 测试方法
test(){
this.testMap['name'] = 'zjw'; // 修改已经存在的键
this.testMap['hobby'] = 'coding'; // 对不存在的键设置值
}

结果:view视图随着test方法的调用会发生变化,name属性会改变,同时hobby属性也会添加进来,dom元素能够及时响应并更新。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 将testMap中的name去掉会怎么样呢?
data() {
return {
testMap:{
'age':22
}
};
// 测试Dom
<van-button @click="test"></van-button>

// 测试方法
test(){
this.testMap['name'] = 'zjw'; // 修改已经存在的键
this.testMap['hobby'] = 'coding'; // 创建属性,对不存在的键设置值
}

结果:view视图随着test方法的调用不会发生变化,数据会发生变化,但是dom元素并未更新,没有得到响应


分析:根据上面两个例子,再结合 **”Vue不能检测到对象的添加或者删除。然而Vue在初始化实例时就对属性执行了setter/getter转化过程,所以属性必须开始就在对象上,这样才能让Vue转化它”**这句话来看,由于第一个例子中name属性在vue实例化时就已经执行转化过程,那么后续对访问器的属性进行操作时,会调用响应的方法,例如读取属性值,会调用getter方法,在修改属性值时会调用setter的方法,这样这些方法就会在底层来决定如何更新数据,包括更新DOM。以此来实现单向绑定及双向绑定。


三 解决方案

为了能够对未经初始化,为执行setter/getter转化过程的属性修改,同时确保这些属性被创建后是响应式的,触发视图view的更新,可以使用Vue.set(Object, String, Any)方法,它的用法是设置对象的属性,如果该对象在data中定义为响应对象,那么该方法确保该对象的属性被创建后也是响应式的,同时触发视图更新。这个方法就可以避开Vue不能检测属性被添加的限制。

{width=”100%”}

所以代码更正为:

1
2
3
4
// 选择合适的map项
choiceValue(index, values) {
this.$set(this.choiceMap, values.name, index);
},

恕我前端不够深入, 推荐一篇讲的更加详细的文章,如有兴趣,可以前去浏览