从拖拽理解vue diff的原理

87 阅读4分钟

众所周知,vue是数据驱动的,数据的改动先反馈到虚拟dom,虚拟dom再通过diff算法来渲染真实dom节点。并且,在数据驱动的框架直接修改dom会引发下面的问题:

  1. 如果改变了真实dom位置之后,再触发渲染,diff又会怎样执行呢?

  2. 改变了真实dom位置之后,把数据也按照真实dom做修改,再执行diff又会怎样呢?

为了解决这些疑问,我们安装sortablejs,然后写一个demo

首先是第一个问题

改变了真实dom位置之后,再触发渲染,diff又会怎样执行呢?

<template>
  <section>
    <div id="sort">
      <div>{{sortArr[0]}}</div>
      <div>{{sortArr[1]}}</div>
      <div>{{sortArr[2]}}</div>
    </div>
    <button @click="id++">refresh: {{id}}</button>
  </section>
</template>

import Sortable from 'sortablejs'

export default {
  name: 'App',
  data() {
    return {
      sortArr: [1, 2, 3],
      id: 0,
    }
  },
  mounted() {
    var el = document.getElementById('sort')
    var sortable = Sortable.create(el)
  },
}

GIF 2023-2-3 10-49-37.gif

diff不会改变dom!!!

diff完全没有作用,这说明,虚拟dom和真实dom之间的是“引用”关系,真实dom虽说改变了位置,但是虚拟dom和真实dom之间的“引用”关系并没有发生改变,diff算法一看,好家伙,虚拟dom和上次完全一样,所以即使再通过虚拟dom重新渲染,真实dom也不会发生变化。

为了研究第二个问题,我们把代码增加逻辑

var sortable = Sortable.create(el, {
  onEnd: ({ newIndex, oldIndex }) => { // 用箭头函数使this指向vue实例
    const currRow = this.sortArr.splice(oldIndex, 1)[0]
    this.sortArr.splice(newIndex, 0, currRow)
  },
})

增加的逻辑就是拖拽改变真实dom后,再将数据同步,使数据和真实dom保持一致

GIF 2023-2-3 11-05-54.gif

可以发现,我进行的操作是将123=>132,但是操作之后,dom突然又变123,和vue里面存的数据不一致!!!即使触发重新渲染,也不会按照vue的数据渲染出正确的dom了,怎么会这样???

接下来我们就来还原下到底发生了什么

刚开始的映射关系是这样的

image.png

拖拽之后,重新渲染前的映射关系是这样的

image.png

关键的一步来了,我们将虚拟dom的数据和真实dom保持一致,所以虚拟dom的2和3换了一下位置,并且在这个时候触发了diff。

diff先patch虚拟dom1,但是1没有发生变化,略过

然后是patch虚拟dom2,注意此时的2指向真实dom的第三个元素

image.png

由于虚拟dom2只是text发生了变化,所以将2改成3,diff同步地将dom3改成了3,所以此时展示的是1,3,3

image.png

同理,下一个是patch虚拟dom3,虚拟dom3指向真实dom的第二个元素,虚拟dom3将3改成2,所以此时展示的是1,2,3,变回去了!!!

这就是数据驱动的框架操作dom之后的后果,虚拟dom和真实dom可能会不一致,所以不要轻易直接修改dom!!!

此时还有一个问题,若是给它们加了key之后会怎样

<div id="sort">
  <div :key="sortArr[0]">{{sortArr[0]}}</div>
  <div :key="sortArr[1]">{{sortArr[1]}}</div>
  <div :key="sortArr[2]">{{sortArr[2]}}</div>
</div>

直接告诉你答案吧,这样写是不行的

image.png

此时虚拟dom2还是指向真实dom3,并且key不相同,会先umount然后再重新创建div,所以开销更大,但还是无法能够修复错乱的映射关系。

那有没有一种可能,正常开发来说,都会采用下面这种写法

<div id="sort">
  <div v-for="item in sortArr" :key="item">{{item}}</div>
</div>

就结果来说,这样是可行的,并且这样子写,diff的过程中,就直接把错乱的映射关系给修复了!连dom的手都没碰到!!!

原理如下:

v-for指令会形成一个Symbol(Fragment)的包装,会把它们一起处理

image.png

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
        let i = 0;
        const l2 = c2.length;
        let e1 = c1.length - 1; // prev ending index
        let e2 = l2 - 1; // next ending index
        // 1. sync from start
        // (a b) c
        // (a b) d e
        while (i <= e1 && i <= e2) {
            const n1 = c1[i];
            const n2 = (c2[i] = optimized
                ? cloneIfMounted(c2[i])
                : normalizeVNode(c2[i]));
            if (isSameVNodeType(n1, n2)) { // 只有key相同的才会patch,所以不会像上面那样key不同就unmount
                patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
            }
            else {
                break;
            }
            i++;
        }
        ……// 一大堆逻辑就是为了能让相同的key patch
        

key相同的才会patch这一点非常关键,直接把错乱的dom映射给修复了

image.png

发现这个问题的背景是写拖拽table的时候,为了方便直接用了sortablejs,并且为了样式统一,还是用的el-table。遇到相同问题的小伙伴,可以给el-table加一个row-key,并在tableData中定义一个key就行