likes
comments
collection
share

Vue2之“诡异”的视图不更新问题

作者站长头像
站长
· 阅读数 29

问题背景描述

今天公司新入职的一个小伙伴问我一个问题: 他在使用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>
  • 正常时效果如下:

Vue2之“诡异”的视图不更新问题

  • 有问题时(即视图不更新时)效果如下:

Vue2之“诡异”的视图不更新问题

问题排查过程

  • 首先我看了数据的结构,起初在定义数据的时候在节点内部没有定义一个expand属性,但是Tree组件内部给每个节点都添加了一个属性expand,并且通过该属性去控制节点的展开或收起。我把节点数据打印到控制台,发现是expand这个属性的gettersetter并没有被劫持,于是我在data中定义数据时,给每个节点都提前加了expand属性,然后看了下效果,没想到视图不更新的问题被解决了。
  • 于是,我查看了同事使用的iView组件库的版本是4.3.2,而我们平常使用的是4.7.0,这时我认为这个问题可能是组件库版本导致的。我到组件库对应的github仓库找关于Tree组件节点展开收起时视图不更新的issue,结果找到的是一些关于在Table组件中树形结构的数据出现的问题,与我目前遇到的这个问题都没什么关系;再之后我就对比了两个版本之间的代码,也没有发现任何问题,而且很诡异的是,组件内部在点击节点的展开收起箭头时,是通过$set方法更改expand的值,如下:
this.$set(this.data, 'expand', !this.data.expand)

Vue2之“诡异”的视图不更新问题

  • 我脑子里出现了一个大大的问号。这是为什么呢?在我目前的认知里,设置一个对象的属性值时,如果使用$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这个属性的gettersetter并没有被劫持,所以我突然明白,本来节点对象是在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;
}

Vue2之“诡异”的视图不更新问题

  • 通过$set方法设置属性值时,会通过in操作符判断该属性在对象中是否存在,如果已经存在,那$set(node, 'expand', true)方法等价于node.expand = true, 两者没有任何区别,到这里问题也就很明了了。由于之前给每个节点都加了expand属性,并且该属性也没有被vue追踪,所以后续在Tree内部即使是使用$set方法设置expand的值,vue始终无法跟踪到该属性,从而也就不会去更新视图。

写在最后

遇到问题时应该多持怀疑态度。

由于自己的知识掌握的不够牢固,而我一开始也并没有对自己的认知产生怀疑,所以后续才需要花更多的时间去确认问题的真正原因。

总而言之,以后学知识必须得深耕,把学到的点吃透才行。

转载自:https://juejin.cn/post/7391324758858301480
评论
请登录