v-for导致的问题引发的思考

726 阅读2分钟

前言

今天有个小伙伴问了我一个他碰到的问题,他在动态添加数组操作的时候 会发生渲染错乱的问题,显示在页面和数组里面的num不一致,引发问题的demo,在点击前面几列的加号的时候 连续点击两次就会问题。如下图 应该为2、3却渲染成3、3(ps:这里不去讨论增减的方法之类,这里他直接改变了tripIndex 所以不能拿这个当做key值 data可能会一样 然后id是我后来加进去的,并不是一开始存在的,当做解决办法)

image.png

附上代码(只是一个简单的vue2和引用了element-ui)

<template>
  <div class="con">
    <div><el-button size='mini' @click='clickEvent'>测试</el-button></el-button></div>
    <div style="width:1000px;padding:50px">
      <el-table :data="tableData" border style="width: 100%" v-if='isShow'>
        <el-table-column
          v-for="(item, index) in tableHeader"
          :key='item.id'
          :prop="item.data"
          width="100"
          align="center"
        >
          <template slot="header" slot-scope="scope">
            <div>
              <i
                @click="addHeade(item.tripIndex)"
                class=" myIcon operateLine-item"
              >+</i>
              <i
                @click="subHeade(item.tripIndex)"
                class=" myIcon"
              >-</i>
            </div>
            {{ `${item.tripIndex}  ${getMain(item)}  ` }}
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script>
export default {
  name: "TestPage",
  components: {},
  data() {
    return {
      isShow:true,
      tableHeader: [
        {
          tripIndex: 1,
          data: "data",
          id:1
        },
        {
          tripIndex: 2,
          data: "data",
          id:2
        },
        {
          tripIndex: 3,
          data: "data",
          id:'3'
        },
        {
          tripIndex: 4,
          data: "data",
          id:'4'
        },
        {
          tripIndex: 5,
          data: "data",
          id:'5'
        }
      ],
      tableData: [
        {
          data: "1"
        }
      ]
    };
  },
  mounted() {},
  methods: {
    clickEvent(){
      console.log(this.tableHeader)
    },
    getMain(v) {
      if (v.tripIndex % 2 == 0) {
        return "副站";
      } else {
        return "主站";
      }
    },
    addHeade(v) {
      this.tableHeader.forEach(element => {
        if (element.tripIndex > v) {
          element.tripIndex += 1;
        }
      });
      this.$nextTick(() => {
        console.log(this.tableHeader);
      });
      console.log(this.tableHeader);

      let addItem = {
        tripIndex: v + 1,
        data: "data",
      };
      console.log(v);
      this.tableHeader.splice(v, 0, addItem);
    },
    subHeade(v) {
      this.tableHeader.splice(v - 1, 1);

      this.tableHeader.forEach(element => {
        if (element.tripIndex > v) {
          element.tripIndex -= 1;
        }
      });
    }
  },
  created() {}
};
</script>

<style scoped>
.con {
  overflow: auto;
  position: relative;
  width: 100%;
  height: 100%;
}
.operateLine-item {
  margin-right: 10px;
}
.myIcon {
  color: #1890ff;
  cursor: pointer;
}
</style>


解决

这种问题其实我有碰到类似的 所以当时就直接给了三个解决方案

  1. this.$forceUpdata()强制刷新 (不过貌似不能成功)
  2. 使用v-if强制刷新

image.png

image.png 3.增加一个唯一标识id,然后在v-for后指明这个id当做key值

:key='item.id'
...
    let addItem = {
        tripIndex: v + 1,
        data: "data",
        id: new Date().getTime()
      };
      

这三种办法后两种是可行的 使用强制刷新重新渲染。 成功demo 虽然有这样的解决方法,但其实我也一知半解的状态所以我要深入思考一下

分析

这是我当时自己的判断 image.png

既然不懂 这时候就要开始百度一下了,这里附上一个很有意思的知乎类似问答 比如这里还有类似问题

image.png 复现demo连接 add三次 click后面两个 然后删除第一个就会有相似渲染问题

搜索到vue官网文档有一句话很有意思

当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项
的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并
且确保它们在每个索引位置正确渲染。这个类似 Vue 1.x 的 track-by="$index"

“就地更新”的策略 这个词可能就让我们很接近答案,就地更新

相关答案 image.png

image.png

我觉得需要模拟ast抽象树 去打印出对应结果(之后有时间来追踪一下)

小结

我自己的结论:diff算法导致的问题,而key值的存在是为了更好的去diff出AST抽象树,当没有key值就会使用就地更新的策略,如果key值混乱也会出现渲染问题。

大佬们要是有明白的又刚好看见此文,还望不吝指教!