【前端每日基础】day38——v-for 的 diff 算法的原理

Vue.js 的 v-for 指令用于渲染列表,内部通过一个高效的 diff 算法来确保 DOM 更新的性能。下面详细介绍 v-for 的 diff 算法原理。

  1. Diff 算法概述
    Diff 算法的目标是在新旧虚拟 DOM 树之间找到最小的变更,并将这些变更应用到实际的 DOM 上。这个过程包括以下步骤:

同层比较:只比较同一层次的节点,不跨层次比较。

节点比较:通过比较节点的 key 值来判断节点是否相同。

最小变更:通过对比新旧节点的不同,生成最小的 DOM 操作集。

  1. Key 的重要性

在使用 v-for 渲染列表时,为每个列表项提供一个唯一的 key 是非常重要的。key 可以帮助 Vue 更准确地识别节点,使 diff 算法能够更加高效地处理节点的增删改查。

html 复制代码
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
  1. Diff 算法的具体步骤

    下面是 Vue 的 diff 算法在 v-for 中处理节点更新的一般过程:

  2. 预处理

    首先,Vue 会将新旧节点列表分别保存在两个数组中,并为每个节点创建一个映射,以 key 为索引,便于快速查找。

javascript 复制代码
const oldChildren = oldVnode.children;
const newChildren = newVnode.children;
  1. 四个指针
    Vue 使用四个指针来进行新旧节点的比较:

oldStartIdx 和 oldEndIdx:指向旧节点列表的开始和结束位置。

newStartIdx 和 newEndIdx:指向新节点列表的开始和结束位置。

  1. 双端比较

通过以下步骤进行双端比较:

比较新旧节点列表的开始节点:如果相同,则更新节点并移动 oldStartIdx 和 newStartIdx。

比较新旧节点列表的结束节点:如果相同,则更新节点并移动 oldEndIdx 和 newEndIdx。

比较旧的开始节点和新的结束节点:如果相同,说明新节点移动到了列表的末尾,需要进行 DOM 操作将旧节点移动到末尾,并移动指针。

比较旧的结束节点和新的开始节点:如果相同,说明新节点移动到了列表的开头,需要进行 DOM 操作将旧节点移动到开头,并移动指针。

  1. 找出中间节点

如果上述比较都不相同,则通过 key 值在旧节点列表中查找新节点。如果找到了对应的旧节点,则更新节点并进行相应的 DOM 操作。否则,将新节点插入到旧节点列表中。

  1. 处理剩余节点
    如果旧节点列表遍历完毕,而新节点列表还有剩余节点,则将剩余的新节点插入到 DOM 中。
    如果新节点列表遍历完毕,而旧节点列表还有剩余节点,则将剩余的旧节点从 DOM 中移除。
  2. 示例代码
    以下是 Vue 3 的 diff 算法的简化示例代码:
javascript 复制代码
function updateChildren(oldChildren, newChildren, parentElm) {
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newEndIdx = newChildren.length - 1;

  let oldStartVnode = oldChildren[oldStartIdx];
  let oldEndVnode = oldChildren[oldEndIdx];
  let newStartVnode = newChildren[newStartIdx];
  let newEndVnode = newChildren[newEndIdx];

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (!oldStartVnode) {
      oldStartVnode = oldChildren[++oldStartIdx];
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIdx];
    } else if (isSameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIdx];
      newStartVnode = newChildren[++newStartIdx];
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIdx];
      newEndVnode = newChildren[--newEndIdx];
    } else if (isSameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode);
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      oldStartVnode = oldChildren[++oldStartIdx];
      newEndVnode = newChildren[--newEndIdx];
    } else if (isSameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode);
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldChildren[--oldEndIdx];
      newStartVnode = newChildren[++newStartIdx];
    } else {
      const elmToMove = oldChildren[idxInOld];
      if (elmToMove) {
        patchVnode(elmToMove, newStartVnode);
        oldChildren[idxInOld] = undefined;
        parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
      } else {
        parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm);
      }
      newStartVnode = newChildren[++newStartIdx];
    }
  }

  if (newStartIdx <= newEndIdx) {
    const before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].elm : null;
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      parentElm.insertBefore(createElm(newChildren[i]), before);
    }
  }

  if (oldStartIdx <= oldEndIdx) {
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      if (oldChildren[i]) {
        parentElm.removeChild(oldChildren[i].elm);
      }
    }
  }
}

function isSameVnode(a, b) {
  return a.key === b.key && a.sel=== b.sel;
}

function patchVnode(oldVnode, newVnode) {
  // 更新 vnode
}

function createElm(vnode) {
  // 创建新元素
}
  1. 结论
    Vue 的 v-for 指令通过高效的 diff 算法来确保列表渲染的性能。关键在于:

使用 key 来唯一标识节点,便于高效的节点查找和更新。

通过四个指针和双端比较策略,最小化 DOM 操作。

处理节点的插入和删除,确保 DOM 与虚拟 DOM 同步。

Vue 的 v-for 指令通过虚拟 DOM 实现高效的 DOM 更新。其 diff 算法的原理是:

同级比较:仅比较相同层级的节点,避免跨层级比较。

键值(key)优化:通过为每个节点设置唯一的 key,Vue 可以更高效地跟踪每个节点的变化,直接复用、移动、删除或添加节点,而不是逐个节点比较。

最小化操作:尽量减少 DOM 操作次数,尽量做到复用现有 DOM 元素。

相关推荐
liuyao_xianhui11 小时前
优选算法_最小基因变化_bfs_C++
java·开发语言·数据结构·c++·算法·哈希算法·宽度优先
黎阳之光11 小时前
数智技术如何赋能空天地一体化,领跑低空经济新赛道
大数据·人工智能·算法·安全·数字孪生
小肝一下11 小时前
每日两道力扣,day2
c++·算法·leetcode·职场和发展
程序员小寒12 小时前
JavaScript设计模式(八):命令模式实现与应用
前端·javascript·设计模式·ecmascript·命令模式
漂流瓶jz12 小时前
UVA-11846 找座位 题解答案代码 算法竞赛入门经典第二版
数据结构·算法·排序算法·深度优先·aoapc·算法竞赛入门经典·uva
wgod12 小时前
new AbortController()
前端
UXbot12 小时前
UXbot 是什么?一句指令生成完整应用的 AI 工具
前端·ai·交互·个人开发·ai编程·原型模式·ux
棒棒的唐12 小时前
WSL2用npm安装的openclaw,无法正常使用openclaw gateway start启动服务的问题
前端·npm·gateway
米粒112 小时前
力扣算法刷题 Day 31 (贪心总结)
算法·leetcode·职场和发展
哔哩哔哩技术12 小时前
使用Compose Navigation3进行屏幕适配
前端