Diff算法
Diff算法即差异查找算法。
Vue的diff策略
- 传统的计算两颗树的差异时间复杂度为O(n^3),显然成本比较高(老树的每一个节点都去遍历新树的节点,直到找到新树对应的节点。那么这个流程就是 O(n^2),再紧接着找到不同之后,再计算最短修改距离然后修改节点,这里是 O(n^3)。)
- Vue采用对树的节点进行同层比较,所以时间复杂度是O(n),比较高效
Vue Diff算法的基于什么策略
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计 (tree-diff)
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结(component diff)
- 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分(element-diff)
Vue Diff算法的原因以及目的
Vue diff算法是vue2中引入虚拟DOM的产物,它的出现是为了通过对比新旧节点计算出需要改动的最小变化。 核心思想:尽可能的复用老节点
Vue2 diff流程
新老节点不同
- 创建新节点 以当前旧节点为参考 插入到DOM
- 删除旧节点
新老节点相同
- 如果两个节点引用一致 直接返回
- 内部都是文本节点 新旧不同 更新文本节点的内容
- 只有新的有子节点 移除旧节点的内容 批量添加
- 只有老的有子节点 批量移除
- 两者都有子节点且不同 执行
updateChildren更新子节点
updateChildren
根据我们日常操作节点的习惯、移动 添加 和 删除 ,Vue2对比两个子节点采用双端比较法,通过对比老节点在新节点的位置尽可能的复用老节点。
- 头头对比 尾尾对比
- 交叉对比 头尾对比
- 新的节点有剩余 进行添加操作
- 老的节点有剩余 进行移除
Vue3的diff流程
新旧节点不同
- 销毁旧节点
- 根据新节点的类型 去挂载不同的节点
处理组件
- 先判断子组件是否需要更新
- 如果需要则递归执行子组件的副渲染函数来更新
- 否则仅仅更新一些 vnode 的属性,并让子组件实例保留对组件 vnode 的引用
处理元素
- 更新props
- 更新子节点 子节点有三种类型 纯文本 Vnode数组 和 空
旧节点是纯文本:
- 新节点也是 做简单的替换
- 新节点是空 删除
- 新节点是Vnode数组 批量添加
旧节点是空:
- 如果新子节点是纯文本,那么在旧子节点的父容器下添加新文本节点即可;
- 如果新子节点也是空,那么什么都不需要做
- 如果新子节点是 vnode 数组,那么直接去旧子节点的父容器下添加多个新子节点即可。
旧子节点是 vnode 数组:
- 如果新子节点是纯文本,那么先删除旧子节点,再去旧子节点的父容器下添加新文本节点;
- 如果新子节点是空,那么删除旧子节点即可
- 如果新子节点也是 vnode 数组,那么就需要做完整的 diff 新旧子节点了,这是最复杂的情况,内部运用了核心 diff 算法
新旧节点都是数组
新旧数组之间的对比,无非是通过更新、删除、添加和移除节点来完成的,diff算法的核心以较低的成本完成子节点的更新为目的,求解生成新子节点 DOM 的系列操作。 过程:
- 同步头节点
- 同步尾节点
- 添加新节点 新子节点有剩余
- 删除多余节点 旧节点有剩余
- 处理未知子序列
处理未知子序列
有时会碰到比较复杂的未知子序列:对于移动、删除、添加、更新这些操作,其中最复杂的就是移动操作,Vue针对未知子序列的核心是通过最长递增子序列查找到需要移动的最小值。
在查找过程中需要对比新旧子序列,那么我们就要遍历某个序列,如果在遍历旧子序列的过程中需要判断某个节点是否在新子序列中存在,这就需要双重循环,而双重循环的复杂度是 O(n2) ,为了优化这个复杂度,我们可以用一种空间换时间的思路,建立索引图,把时间复杂度降低到 O(n)。
建立索引图
- 根据for循环中的key建立新子序列中的索引图
- 然后再创建一个新旧子序列索引的映射关系,用于确定最长递增子序列、
- 然后正序遍历旧子序列,看看是否在新子序列的索引图中,如果不再就删除,如果在根据索引去判断这个节点是否在最长递增子序列中,如果在就不需要进行移动,如果不再就要进行移动操作
- 然后在遍历的过程中对新节点打上标记,对于没有被查找的标识为0,需要进行添加操作