[记录]我的Diff算法学习路径

简介: [记录]我的Diff算法学习路径

Catalog

  1. Vue.js
    1.1 patch 流程
    1.2 updateChildren 大概步骤
    1.3 updateChildren 代码实现
    1.4 两端比对 DEMO
  2. React.js
    2.1 updateChildren 大概步骤
    2.2 updateChildren 代码实现
    2.3 节点移动、新增 、移除 DEMO
  3. Reference

Vue.js Diff 算法

patch 流程

- oldVNode 判断 是否存在
  - oldVNode 存在
    - VNode 判断 是否存在
      - VNode 存在
        - patch
          - sameVnode 判断 key && tag && isComment && isDef(a.data) && sameInputType(a, b) 基本属性是否相同
            - sameVnode 相同
              - diff
                - VNode 判断 是否是文本节点
                  - 是,若 oldVNode.text !== VNode.text 则直接进行文本节点替换
                  - 否,进入子节点 diff
                    - oldCh 不存在,cn 存在,清空 oldVNode 的文本节点,同时调用 addVnodes 方法将 cn 添加到 elm 真实的 dom 节点中
                    - oldCh 节点存在,cn 不存在,则删除 elm 真实节点下的 oldCn 子节点
                    - oldCh 存在,cn 存在,调用 updateChildren 对子节点进行 diff
                    - oldVNode 有文本节点,vnode 没有,那么就清空这个文本节点
            - sameVnode 不相同
              - 删除老的 dom 节点,生成新的 dom
      - VNode 不存在
        - 移除 DOM
  - oldVNode 不存在
    - 挂载 DOM

updateChildren 大概步骤

  1. 建立两组首尾索引和节点变量
  2. 当满足 oldStartIdx <= oldEndIdx 并且 newStartIdx <= newEndIdx 条件时进入循环

    1. 双端比较 => 循环中先进行比较找出 key 相同的新旧节点,根据四种情况判断是否移动 DOM 和增减首尾索引变量以及 patch
    2. 有 key 时 => 当双端匹配没有找到 key 相同的节点,通过当前 newStartVNode 的 key 在 nextChildren 中寻找 key 相同的 VNode

      1. key 相同的 VNode 存在时,对当前节点进行移动 DOM、patch 并将旧 children 中该位置的节点设为 undefined,将 newStartIdx 下移一位
      2. key 相同的 VNode 不存在,则当前“第一个”节点为新节点,将其挂载到 oldStartVNode 之前,将 newStartIdx 下移一位
  3. 挂载全新的节点

    1. 当 oldEndIdx < oldStartIdx, 添加新节点
  4. 移除不存在的节点

    1. 当 newEndIdx < newStartIdx, 移除不存在的元素

updateChildren 代码实现

// 建立两组首尾索引
let oldStartIdx = 0;
let oldEndIdx = prevChildren.length - 1;
let newStartIdx = 0;
let newEndIdx = nextChildren.length - 1;
// 索引对应的VNode
let oldStartVNode = prevChildren[oldStartIdx];
let oldEndVNode = prevChildren[oldEndIdx];
let newStartVNode = nextChildren[newStartIdx];
let newEndVNode = nextChildren[newEndIdx];
// 进行执行双端比较-简略流程版
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (oldStartVNode.key === newStartVNode.key) {
    // 步骤一:oldStartVNode 和 newStartVNode 比对
    ...
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 步骤二:oldEndVNode 和 newEndVNode 比对
    ...
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 步骤三:oldStartVNode 和 newEndVNode 比对
    ...
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 步骤四:oldEndVNode 和 newStartVNode 比对
    ...
  }else {
    // 当双端比对没有匹配节点时,遍历旧 children,试图寻找与 newStartVNode 拥有相同 key 值的元素
  }
}
// 进行执行双端比较-详细流程版
// 处理捕获,移动DOM,增减序号
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (!oldStartVNode) {
    // 当旧VNode已被diff移到别的位置时,旧VNode节点为undefined需要跳过当前索引
    oldStartVNode = prevChildren[++oldStartIdx]
  } else if (!oldEndVNode) {
    // 当旧VNode已被diff移到别的位置时,旧VNode节点为undefined需要跳过当前索引
    oldEndVNode = prevChildren[--oldEndIdx]
  } else if (oldStartVNode.key === newStartVNode.key) {
    // 步骤一:oldStartVNode 和 newStartVNode 比对
    // 调用 patch 函数更新
    patch(oldStartVNode, newStartVNode, container)
    // 更新索引,指向下一个位置
    oldStartVNode = prevChildren[++oldStartIdx]
    newStartVNode = nextChildren[++newStartIdx]
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 步骤二:oldEndVNode 和 newEndVNode 比对
    // 调用 patch 函数更新
    patch(oldEndVNode, newEndVNode, container)
    // 更新索引,指向下一个位置
    oldEndVNode = prevChildren[--oldEndIdx]
    newEndVNode = nextChildren[--newEndIdx]
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 步骤三:oldStartVNode 和 newEndVNode 比对
    // 调用 patch 函数更新
    patch(oldStartVNode, newEndVNode, container)
    // 将 oldStartVNode.el 移动到 oldEndVNode.el 的后面,也就是 oldEndVNode.el.nextSibling 的前面
    container.insertBefore(
      oldStartVNode.el,
      oldEndVNode.el.nextSibling
    )
    // 更新索引,指向下一个位置
    oldStartVNode = prevChildren[++oldStartIdx]
    newEndVNode = nextChildren[--newEndIdx]
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 步骤四:oldEndVNode 和 newStartVNode 比对
    // 先调用 patch 函数完成更新
    patch(oldEndVNode, newStartVNode, container)
    // 更新完成后,将容器中最后一个子节点移动到最前面,使其成为第一个子节点
    container.insertBefore(oldEndVNode.el, oldStartVNode.el)
    // 更新索引,指向下一个位置
    oldEndVNode = prevChildren[--oldEndIdx]
    newStartVNode = nextChildren[++newStartIdx]
  } else {
    // 当双端比对没有匹配节点时,遍历旧 children,试图寻找与 newStartVNode 拥有相同 key 值的元素
    const idxInOld = prevChildren.findIndex(
      node => node.key === newStartVNode.key
    )
    if (idxInOld >= 0) {
      // vnodeToMove 就是在旧 children 中找到的节点,该节点所对应的真实 DOM 应该被移动到最前面
      const vnodeToMove = prevChildren[idxInOld]
      // 调用 patch 函数完成更新
      patch(vnodeToMove, newStartVNode, container)
      // 把 vnodeToMove.el 移动到最前面,即 oldStartVNode.el 的前面
      container.insertBefore(vnodeToMove.el, oldStartVNode.el)
      // 由于旧 children 中该位置的节点所对应的真实 DOM 已经被移动,所以将其设置为 undefined
      prevChildren[idxInOld] = undefined
    } else {
      // 挂载新节点,当双端比对与key查找都不匹配时 newStartVNode 即是新节点
      // newStartVNode 是这轮比较中的“第一个”节点,所以把它挂在到 oldStartVNode 前即可
      mount(newStartVNode, container, false, oldStartVNode.el)
    }
    // 将 newStartIdx 下移一位
    newStartVNode = nextChildren[++newStartIdx]
  }
}
// 挂载没有被处理的全新节点
if (oldEndIdx < oldStartIdx) {
  // 循环结束后当 oldEndIdx < oldStartIdx, 添加新节点
  for (let i = newStartIdx; i <= newEndIdx; i++) {
    mount(nextChildren[i], container, false, oldStartVNode.el)
  }
} else if (newEndIdx < newStartIdx) {
  // 循环结束后当 newEndIdx < newStartIdx, 移除不存在的元素
  for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    container.removeChild(prevChildren[i].el)
  }
}

两端比对 DEMO

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { diff... }

VNode startIndex/endIndex
a b c d 0 / 3
d b a c 0 / 3
next next
a b c x 0 / 2
x b a c 1 / 3
next next
a b x x 0 / 0
x b a x 2 / 2
next next
a x x x 1 / 0
x x a x 1 / 0
next next
x x x x 0 / 0
x x x x 0 / 0
complete complete

两端比对 - 添加新元素 DEMO

VNode startIndex/endIndex
a b 0 / 1
a d b 0 / 2
next next
A b 1 / 1
A d b 1 / 2
next next
A B 1 / 0
A d B 1 / 1
next next
A d B 1 / 0
A d B 1 / 1
complete complete

两端比对 - 添加新元素 DEMO2

VNode startIndex/endIndex
a b c 0 / 2
d a b c 0 / 3
next next
a b C 0 / 1
d a b C 0 / 2
next next
a B C 0 / 0
d a B C 0 / 1
next next
A B C 0 / -1
d A B C 0 / 0
next next
d A B C 0 / -1
d A B C 0 / 0
next next
complete complete

两端比对 - 移除不存在的元素 DEMO

VNode startIndex/endIndex
a b c 0 / 2
a c 0 / 1
next next
A b c 1 / 2
A c 1 / 1
next next
A b C 1 / 1
A C 1 / 0
next next
A C 1 / 1
A C 1 / 0
next next
complete complete

React.js Diff 算法

updateChildren 大概步骤

  1. 初始化最大索引值变量(lastIndex)为 0
  2. 遍历新节点数组,初始化是否匹配标识符(find)为 false

    1. 循环旧节点数组,查找 key 相同的节点

      1. 存在,先进行 patch,后判断节点是否需要移动(j<lastIndex)

        1. 需要移动=>移动 DOM
        2. 不需要移动=>更新 lastIndex 变量为当前索引,连接新节点到对应 DOM 上
      2. 不存在,则为新节点,需要挂载 DOM 到上一个新节点的后面
    2. 处理已经不存在的节点

      1. 遍历旧节点数组,移除新节点数组中不存在的节点

updateChildren 代码实现

// 用来存储寻找过程中遇到的旧节点数组中最大索引值
let lastIndex = 0;
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
  const nextVNode = nextChildren[i];
  let j = 0,
    find = false;
  // 遍历旧的 children
  for (j; j < prevChildren.length; j++) {
    const prevVNode = prevChildren[j];
    // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
    if (nextVNode.key === prevVNode.key) {
      find = true;
      patch(prevVNode, nextVNode, container);
      // 判断节点是否需要移动
      if (j < lastIndex) {
        // 需要移动
        // refNode 是为了下面调用 insertBefore 函数准备的
        const refNode = nextChildren[i - 1].el.nextSibling;
        // 调用 insertBefor 函数移动 DOM,将当前节点的el 移动到 refNode前
        container.insertBefore(prevVNode.el, refNode);
      } else {
        // 更新 lastIndex
        lastIndex = j;
        // 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
        const el = (nextVNode.el = prevVNode.el);
      }
      break; // 这里需要 break
    }
    // 挂载新节点
    if (!find) {
      // 找到 refNode
      const refNode =
        i - 1 < 0 ? prevChildren[0].el : nextChildren[i - 1].el.nextSibling;
      mount(nextVNode, container, false, refNode);
    }
  }
  // 移除已经不存在的节点
  // 遍历旧的节点
  for (let i = 0; i < prevChildren.length; i++) {
    const prevVNode = prevChildren[i];
    // 拿着旧 VNode 去新 children 中寻找相同的节点
    const has = nextChildren.find(
      (nextVNode) => nextVNode.key === prevVNode.key
    );
    if (!has) {
      // 如果没有找到相同的节点,则移除
      container.removeChild(prevVNode.el);
    }
  }
}

节点移动、新增、移除 DEMO

VNode lastIndex 是否需要移动 DOM 是否新增 DOM 是否移除 DOM
a b d / / / /
a d c 0 n n n
next d next next next
a b d / / / /
a d c 2 n n n
next c next next next
a b d / / / /
a d c 2 n y n
next 移除旧 DOM next next next
a b d c / / / y
a d c / / / /
next next next next next
a b c / / / /
a d c / / / /
complete complete complete complete complete

Reference

目录
相关文章
|
26天前
|
JavaScript 前端开发 算法
React技术栈-虚拟DOM和DOM diff算法
这篇文章介绍了React技术栈中的虚拟DOM和DOM diff算法,并通过一个实际案例展示了如何使用React组件和状态管理来实现动态更新UI。
34 2
|
29天前
|
JavaScript 前端开发 算法
react中虚拟dom和diff算法
在React中,虚拟DOM(Virtual DOM)和Diff算法是两个核心概念,它们共同工作以提高应用的性能和效率。
30 4
|
3天前
|
XML JavaScript 前端开发
学习react基础(1)_虚拟dom、diff算法、函数和class创建组件
本文介绍了React的核心概念,包括虚拟DOM、Diff算法以及如何通过函数和类创建React组件。
14 2
|
30天前
|
前端开发 算法 JavaScript
React原理之Diff算法
【8月更文挑战第24天】
|
1天前
|
机器学习/深度学习 JavaScript 算法
面试中的网红虚拟DOM,你知多少呢?深入解读diff算法
该文章深入探讨了虚拟DOM的概念及其diff算法,解释了虚拟DOM如何最小化实际DOM的更新,以此提升web应用的性能,并详细分析了diff算法的实现机制。
|
1月前
|
JavaScript 算法 前端开发
"揭秘Vue.js的高效渲染秘诀:深度解析Diff算法如何让前端开发快人一步"
【8月更文挑战第20天】Vue.js是一款备受欢迎的前端框架,以其声明式的响应式数据绑定和组件化开发著称。在Vue中,Diff算法是核心之一,它高效计算虚拟DOM更新时所需的最小实际DOM变更,确保界面快速准确更新。算法通过比较新旧虚拟DOM树的同层级节点,递归检查子节点,并利用`key`属性优化列表更新。虽然存在局限性,如难以处理跨层级节点移动,但Diff算法仍是Vue高效更新机制的关键,帮助开发者构建高性能Web应用。
47 1
|
1月前
|
JavaScript 算法 索引
【Vue面试题二十三】、你了解vue的diff算法吗?说说看
这篇文章深入分析了Vue中的diff算法,解释了其在新旧虚拟DOM节点比较中的工作机制,包括同层节点比较、循环向中间收拢的策略,并通过实例演示了diff算法的执行过程,同时提供了源码层面的解析,说明了当数据变化时,如何通过Watcher触发patch函数来更新DOM。
【Vue面试题二十三】、你了解vue的diff算法吗?说说看
|
1月前
|
算法 Java
掌握算法学习之字符串经典用法
文章总结了字符串在算法领域的经典用法,特别是通过双指针法来实现字符串的反转操作,并提供了LeetCode上相关题目的Java代码实现,强调了掌握这些技巧对于提升算法思维的重要性。
|
1月前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
23天前
|
算法 BI Serverless
基于鱼群算法的散热片形状优化matlab仿真
本研究利用浴盆曲线模拟空隙外形,并通过鱼群算法(FSA)优化浴盆曲线参数,以获得最佳孔隙度值及对应的R值。FSA通过模拟鱼群的聚群、避障和觅食行为,实现高效全局搜索。具体步骤包括初始化鱼群、计算适应度值、更新位置及判断终止条件。最终确定散热片的最佳形状参数。仿真结果显示该方法能显著提高优化效率。相关代码使用MATLAB 2022a实现。