vue2 diff原理

38 阅读3分钟

考虑如下代码:

<template>
  <div id="app">
    <div>
      <ul>
        <li v-for="item in items" :key="item.id">{{ item.val }}</li>
      </ul>
    </div>
    <button @click="change">change</button>
  </div>
</template>

<script>
  export default {
    name: 'App',
    data() {
      return {
        items: [
          {id: 0, val: 'A'},
          {id: 1, val: 'B'},
          {id: 2, val: 'C'},
          {id: 3, val: 'D'}
        ]
      }
    },
    methods: {
      change() {
        this.items.reverse().push({id: 4, val: 'E'})
      }
    }
  }
</script>

change方法执行,Vue怎么更新DOM呢?是直接覆盖吗?

Vue内部更新DOM有一套自己的diff算法,通过对比新旧节点执行不同的逻辑

这里的新旧节点指的是根节点,比如上面的例子中<div id="app">就是根节点,Vue会从根节点开始,从父到子依次遍历节点

首先需要对比新旧节点是否相同,步骤如下:

  1. 判断两个节点的key是否相等,如果相同,执行下一步
  2. 判断两个节点的tag是否相等,如果相同,执行下一步
  3. 遍历两个节点的的属性,判断属性是否相等,如果相同,执行下一步
  4. 如果到了这一步,则两个节点相同,否则两个节点不同

然后根据新旧节点是否相同执行不同的逻辑


新旧节点不同


如下两个节点:

graph TD; Root1-->A; Root1-->B; Root1-->C
graph TD; Root2-->A; Root2-->B; Root2-->C

Root1Root2不同,即使他们的子节点相同

这种情况非常简单,直接把Root1替换成Root2即可


新旧节点相同


这个时候需要获取它们的children,然后分不同情况考虑

旧children不存在,新children存在

如下两个节点:

graph TD; Root
graph TD; Root-->A; Root-->B; Root-->C

只需把ABC批量插入到原有的Root下即可

旧children存在,新children不存在

如下两个节点:

graph TD; Root-->A; Root-->B; Root-->C
graph TD; Root

只需把ABC删除即可

新、旧children都存在

如下两个节点:

graph TD; Root-->A; Root-->B; Root-->C; Root-->D
graph TD; Root-->D; Root-->C; Root-->B; Root-->A; Root-->E

这种情况最复杂,Vue会通过双指针算法来更新children

第一步:

image.png 定义四个指针分别指向新旧children头尾

第二步:

image.png 分别对比oldStartIndexnewStartIndexoldEndIndexnewEndIndexoldStartIndexnewEndIndexoldEndIndexnewStartIndex是否相同(对比方法和上面的一样)

类似于这样:

image.png

对比过程中如果找到相同的节点,如果是start和end相同,就将旧的节点移到新节点的位置,如果是start和start或end和end相同,则不变

比如上面:找到D相同,将旧节点D移到新节点D的位置(在oldStartIndex对应的A之前插入D),然后将找到相同节点的指针往内部移动

对比过程中如果没有找到相同的节点,则将newStartIndex对应的元素在旧children中查找是否存在,如果存在,将旧的节点移到新节点的位置(在oldStartIndex之前插入),然后将找到相同节点的指针往内部移动

如果还没有找到相同的节点,则将newStartIndex对应的元素加入到旧children对应位置(在oldStartIndex之前插入),然后将newStartIndex指针往内部移动

第三步:

image.png

重复第二步的步骤,找到C相同,将旧节点C移到新节点C的位置,oldEndIndex指针和newStartIndex指针往内部移动

第四步:

image.png

重复第二步的步骤,找到B相同,将旧节点B移到新节点B的位置,oldEndIndex指针和newStartIndex指针往内部移动

第五步:

image.png

重复第二步的步骤,找到A相同,新旧A都在start位置,不用动,oldStartIndex指针和newStartIndex指针往内部移动

第六步:

image.png

重复第二步的步骤,oldStartIndexnewStartIndexoldEndIndexnewEndIndexoldStartIndexnewEndIndexoldEndIndexnewStartIndex都不相同,则将newStartIndex对应元素E在旧children中查找是否存在,不存在,则将newStartIndex对应元素E加入到旧children对应位置(在oldStartIndex之前插入E


总结


可以看到,diff的核心就是对比新旧节点,如果不同就替换,如果相同获取它们的children,根据不同情况做不同的更新逻辑,然后递归再对比children的children,这样就遍历整个DOM树完成diff