Vue2之“诡异”的视图不更新问题
问题背景描述
今天公司新入职的一个小伙伴问我一个问题: 他在使用view-design组件库中Tree组件的时候,把一个静态数组(在组件内部写死的数组)自行处理成树形结构的数据,但是在点击树节点的展开箭头时,该节点并没有如期的展开或者收起。代码逻辑大概如下:
<template>
<div>
<Tree :data="treeData"></Tree>
</div>
</template>
<script>
export default {
name: 'TestTree',
components: {},
props: {},
data() {
return {
dataList: [
{
id: 1,
title: '根节点',
},
{
id: 2,
title: '子节点-101',
parentId: 1,
},
{
id: 3,
title: '子节点-102',
parentId: 1,
},
{
id: 4,
title: '子节点-103',
parentId: 1,
},
{
id: 5,
title: '子节点-301',
parentId: 3,
},
],
}
},
computed: {
treeData() {
return this.handleData(this.dataList)
},
},
watch: {},
methods: {
handleData(data) {
if (!Array.isArray(data)) return []
let r = []
data.forEach((i) => {
i.children = data.filter((j) => j.parentId === i.id)
let p = data.find((j) => j.id === i.parentId)
if (!p) r.push(i)
})
return r
},
},
created() {},
mounted() {},
}
</script>
<style lang="less" scoped>
.set-nochange-view {
padding: 16px;
text-align: center;
}
</style>
- 正常时效果如下:
- 有问题时(即视图不更新时)效果如下:
问题排查过程
- 首先我看了数据的结构,起初在定义数据的时候在节点内部没有定义一个expand属性,但是Tree组件内部给每个节点都添加了一个属性
expand
,并且通过该属性去控制节点的展开或收起。我把节点数据打印到控制台,发现是expand
这个属性的getter
和setter
并没有被劫持,于是我在data中定义数据时,给每个节点都提前加了expand
属性,然后看了下效果,没想到视图不更新的问题被解决了。 - 于是,我查看了同事使用的iView组件库的版本是4.3.2,而我们平常使用的是4.7.0,这时我认为这个问题可能是组件库版本导致的。我到组件库对应的github仓库找关于Tree组件节点展开收起时视图不更新的issue,结果找到的是一些关于在Table组件中树形结构的数据出现的问题,与我目前遇到的这个问题都没什么关系;再之后我就对比了两个版本之间的代码,也没有发现任何问题,而且很诡异的是,组件内部在点击节点的展开收起箭头时,是通过$set方法更改
expand
的值,如下:
this.$set(this.data, 'expand', !this.data.expand)
-
我脑子里出现了一个大大的问号。这是为什么呢?在我目前的认知里,设置一个对象的属性值时,如果使用$set方法,它肯定会触发视图的更新,但是这里为什么不会呢?我只能从另外的角度切入去排查问题。
-
我自己重新写了个把列表数据处理成树形数据的方法,然后替换同事写的方法,结果发现,我的方法处理数据后没出现视图不更新的问题,一切正常。但是之前看过同事的方法,他处理数据的逻辑也没什么问题,只是处理的方式和我有些许不同,为什么会这样,我就再重新看了遍他写的方法,就看到他在处理数据过程中遍历节点时给每个节点都设置了
expand
属性,我持着怀疑的态度把i.expand = true
这一行注掉试了一下,嘿好家伙,没问题了。
// 同事写的,问题就出现在expand属性上
handleData(data) {
if (!Array.isArray(data)) return []
let r = []
data.forEach((i) => {
// 同事在处理数据时,在此处给每个节点设置了一个expand属性,而这也导致了问题的出现
i.expand = true
i.children = data.filter((j) => j.parentId === i.id)
let p = data.find((j) => j.id === i.parentId)
if (!p) r.push(i)
})
return r
},
// 我写的,没有添加expand属性
handleData(data) {
if (!Array.isArray(data)) return []
let r = []
data.forEach((i) => {
i.children = data.filter((j) => j.parentId === i.id)
let p = data.find((j) => j.id === i.parentId)
if (!p) r.push(i)
})
return r
},
分析问题出现原因
- 为什么添加了一行
i.expand=true
的代码会导致视图不更新呢?回想起在控制台打印节点数据的时候expand
这个属性的getter
和setter
并没有被劫持,所以我突然明白,本来节点对象是在data中已经定义好的,每个节点在组件初始化时都被初始化成了响应是对象,那这里通过i.expand=true
的方式给节点添加属性,那不就是给响应式对象直接添加一个新属性,vue无法追踪到该属性的这个问题吗?我的脑袋一下开阔了。 - 但是我又有了一个疑问那就是即使这里无法去追踪该属性的变化,但是Tree组件内部依然是使用的
$set
方法呀,为什么也不去更新视图呢?想到这里,我只能从$set
方法出发去看看问题,会不会是$set
在设置属性值的逻辑跟我的认知有点不一样?于是我看了$set
方法的源码,结果还真是,直接上源码:
function set(target, key, val) {
if ((isUndef(target) || isPrimitive(target))) {
warn$2("Cannot set reactive property on undefined, null, or primitive value: ".concat(target));
}
if (isReadonly(target)) {
warn$2("Set operation on key \"".concat(key, "\" failed: target is readonly."));
return;
}
var ob = target.__ob__;
if (isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
// when mocking for SSR, array methods are not hijacked
if (ob && !ob.shallow && ob.mock) {
observe(val, false, true);
}
return val;
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
if (target._isVue || (ob && ob.vmCount)) {
warn$2('Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.');
return val;
}
if (!ob) {
target[key] = val;
return val;
}
defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock);
{
ob.dep.notify({
type: "add" /* TriggerOpTypes.ADD */,
target: target,
key: key,
newValue: val,
oldValue: undefined
});
}
return val;
}
- 通过
$set
方法设置属性值时,会通过in操作符判断该属性在对象中是否存在,如果已经存在,那$set(node, 'expand', true)
方法等价于node.expand = true
, 两者没有任何区别,到这里问题也就很明了了。由于之前给每个节点都加了expand
属性,并且该属性也没有被vue追踪,所以后续在Tree内部即使是使用$set
方法设置expand
的值,vue始终无法跟踪到该属性,从而也就不会去更新视图。
写在最后
遇到问题时应该多持怀疑态度。
由于自己的知识掌握的不够牢固,而我一开始也并没有对自己的认知产生怀疑,所以后续才需要花更多的时间去确认问题的真正原因。
总而言之,以后学知识必须得深耕,把学到的点吃透才行。
转载自:https://juejin.cn/post/7391324758858301480