VUE--diff算法(中文简版)

173 阅读2分钟

diff 算法是在数据变动时,逐层比较新旧虚拟节点,再决定添加或删除或重复使用真实节点,添加 key 可以提高节点的利用率,但同时会增加计算量。

vue 实例在创建的时候会为 data 添加 get、set 方法,data 改变时会触发 set 方法,set 方法通知所有的订阅者,订阅者调用 patch 给真实节点打补丁。

数据更新时:

  // Vue.prototype._update

  获取真实节点;
  获取旧虚拟节点;
  if (没有旧虚拟节点) {
    patch(真实节点, 新虚拟节点);
  } else {
    // 对新旧虚拟节点进行 diff 比较
    patch(旧虚拟节点, 新虚拟节点);
  }

看看 patch 函数是什么样的:

  // patch

  if (旧虚拟节点不存在) {
    创建新真实节点;
  } else {
    比较新旧虚拟节点是否相同;
    if (相同) {
      // 递归比较
      patchVnode();
    } else {
      用新虚拟节点创建新真实节点;
      用旧虚拟节点删除旧真实节点;
    }
  }

接下来是 patchVnode 函数:

  // patchVnode

  if (新旧虚拟节点相同) {
    直接返回;
  } else {
    保存旧虚拟节点引用的旧真实节点;
    if (新虚拟节点不是文本节点) {
      if (新旧虚拟节点都有子节点且子节点不相等) {
        递归执行 updateChildren 函数比较子节点;
      } else if (只有新虚拟节点有子节点) {
        if (旧虚拟节点为文本) {
          旧真实节点置空;
        }
        插入新的真实节点;
      } else if (只有旧虚拟节点有子节点) {
        移除旧真实节点的所有子节点;
      } else if (旧虚拟节点存在且是文本节点) {
        旧真实节点置空;
      }
    } else if (新虚拟节点的文本内容和旧虚拟节点不一样) {
      把旧虚拟节点设置为新虚拟节点文本内容
    }
  }

diff 算法的核心—— updateChildren 函数:

  // updateChildren

  旧头: 旧虚拟节点的第一个子节点;
  旧尾: 旧虚拟节点的最后一个子节点;
  新头: 新虚拟节点的第一个子节点;
  新尾: 新虚拟节点的最后一个子节点;

  // 遍历新旧虚拟节点的子节点来比较和更新
  while (旧头索引 <= 旧尾索引 && 新头索引 <= 新尾索引) {
    if (旧头为空) {
      旧虚拟节点移除第一个子节点;
    } else if (旧尾为空) {
      旧虚拟节点移除最后一个子节点;
    } else if (旧头新头相同) {
      执行 patchVnode 递归下去;
      新旧虚拟节点都移除第一个子节点;
    } else if (旧尾新尾相同) {
      执行 patchVnode 递归下去;
      新旧虚拟节点都移除最后一个子节点;
    } else if (旧头新尾相同) {
      执行 patchVnode 递归下去;
      旧头索引向右移动,新尾索引向左移动;
    } else if (旧尾新头相同) {
      执行 patchVnode 递归下去;
      旧尾索引向左移动,新头索引向右移动;
    } else {
      // 若 4 个虚拟节点都不相同,就利用 key 去对比
      用旧的虚拟节点的 key 做 map 映射;
      // 循环过程中新虚拟节点的每个子节点都会成为新头
      新头的 key 和 map 做对比;
      if (key in map) {
        if (是相同节点) {
          继续进行 patchVnode 操作,再执行 insertBefore 插入相应位置的真实节点;
        } else {
          创建新的真实节点并插入;
        }
      } else {
        创建新的真实节点并插入;
      }
    }
  }

  // 循环结束后,可能存在未处理的虚拟子节点
  if (旧头索引 > 旧尾索引) {
    表示新虚拟节点的子节点未处理完,遍历剩余的新虚拟节点的子节点,插入到新真实节点中;
  } else if (新头索引 > 新尾索引) {
    表示旧虚拟节点的子节点未处理完,删除旧真实节点中对应的子节点
  }

参考: