Vue中的Diff算法

2023-11-12

Vue中的Diff算法

本篇文章主要介绍Diff算法的思想和Vue中对Diff算法的基本实现。

1. 为什么要用Diff算法

由于在浏览器中操作DOM的代价是非常“昂贵”的,所以才在Vue引入了Virtual DOM,Virtual DOM是对真实DOM的一种抽象描述,不懂的朋友可以自行查阅相关资料。

即使使用了Virtual DOM来进行真实DOM的渲染,在页面更新的时候,也不能全量地将整颗Virtual DOM进行渲染,而是去渲染改变的部分,这时候就需要一个计算Virtual DOM树改变部分的算法了,这个算法就是Diff算法。

2. 传统的Diff算法

传统的Diff算法通过循环递归对节点进行比较,然后判断每个节点的状态以及要做的操作(add,remove,change),最后 根据Virtual DOM进行DOM的渲染。大体流程如下图(图来源):
image

传统Diff算法的复杂度为O(n^3),这个复杂度相对来说还是较高的。后来React开发者提供了一种复杂度仅为O(n) 的Diff算法。下面就来看一下O(n)复杂度的Diff算法是如何实现的。

3. 更高效的Diff算法

React的开发者结合Web界面的特点做出了两个大胆的假设,使得Diff算法复杂度直接从O(n^3)降低到O(n),假设如下:

  • 两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构;
  • 对于同一层次的一组子节点,它们可以通过唯一的id进行区分。

通过这两个假设,他们提供了下面的Diff算法思路。

同层比较

新的Diff算法是逐层进行比较,只比较同一层次的节点,大大降低了复杂度,具体如下图。在后面的内容中也会介绍Vue中同层节点比较的具体实现。
同层比较

不同类型节点的比较

如果发现新旧两个节点类型不同时,Diff算法会直接删除旧的节点及其子节点并插入新的节点,这是由于前面提出的不同组件产生的DOM结构一般是不同的,所以可以不用浪费时间去比较。注意的是,删除节点意味着彻底销毁该节点,并不会将该节点去与后面的节点相比较。

相同类型节点的比较

若是两个节点类型相同时,Diff算法会更新节点的属性实现转换。

列表节点的比较

列表节点的操作一般包括添加、删除和排序,列表节点需要我们给它一个key才能进行高效的比较。

4.Vue Diff算法的实现

了解了Diff算法的大体思路后,我们回过头来看下Vue中的Diff算法是如何实现的。

Vue的Diff算法与上面的思路大体相似,只比较同级的节点,若找不到与新节点类型相同的节点,则插入一个新节点,若有相同类型的节点则进行节点属性的更新,最后删除新节点列表中不包含的旧节点。具体的实现在vue源码的src/core/vdom/patch.js中的updateChildren方法中,由于代码较长,下面简单说一下整个的比较流程。

初始化

图1

如上图,有一组新旧节点数组before:[A, B, C, D]、after:[E, C, F, G],我们设置了四个哨兵节点,oldStartIdx、newStartIdx、oldEndIdx、newEndIdx分别指向新旧节点数组的起始下标和开始下标,值为0,0,3,3;oldStartVnode,newStartVnode,oldEndVnode,newEndVnode则分别指向了before和after节点列表中对应哨兵节点下标的值,值为before[oldStartVnode],after[newStartIdx],before[oldEndIdx],after[newEndIdx]。

Diff

当哨兵满足oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx的条件的时候,我们会循环进行一系列节点之间的比较。

优先判断

我们首先对上面声明的各个节点进行一些优先级较高的判断。

  • 判断1:oldStartVnode是否为空,若为true则oldStartIdx向后移动,继续下一个节点的判断。判断代码如下:
if (isUndef(oldStartVnode)) {
    // 更新哨兵
    oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
}
  • 判断2:oldEndVnode是否为空,若为true则oldEndIdx向前移动。判断代码如下:
else if (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]
}
  • 判断3:使用 sameVnode判断before和after未判断的头节点是否为相同节点,若为true,则按照上面思路说的,对相同类型节点进行节点的属性的更新并修改哨兵位置。
// sameVnode为判断节点是否相等的方法,包括key、tag、isComment等各个属性的相等才能算作相同节点
else if (sameVnode(oldStartVnode, newStartVnode)) {
    // 更新节点内容
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
    // 更新哨兵
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]
}
  • 判断4:使用上一步相同的方法对oldEndVnode和newEndVnode进行判断。并执行相同的更新操作。
else if (sameVnode(oldEndVnode, newEndVnode)) {
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
    // 更新哨兵
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
}
  • 判断5:使用sameVNode判断旧列表的头节点和新列表的尾节点进行判断,
    sameVnode(oldStartVnode, newEndVnode),若为true,更新相同节点,若该节点可以移动在真实DOM中将oldStartVnode,放到真实节点列表的最后。
 else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
    // 真实DOM移动到真实节点列表的最后面
    canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
    // 更新哨兵
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
}
  • 判断6:使用sameVnode比较旧列表的尾节点和新列表的头节点,若为true,和上面一样,更新相同节点,将oldEndVnode放到真实节点列表的最开始。
 else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
    // 真实DOM移动到真实节点列表最前面
    canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
    oldEndVnode = oldCh[--oldEndIdx]
    newStartVnode = newCh[++newStartIdx]
} 

通过这一系列的优先判断条件,一方面对于一些不需要做移动的DOM可以得到快速处理,另一方面使待处理节点变少,缩小了后续操作的处理范围,可以更快地完成同级节点的对比。

若节点不满足上面的所有判断,则会进入到最后一个条件分支,判断7:。

else {
    // oldKeyToIdx为after列表中key和index的映射,可以加快查找速度
    if (isUndef(oldKeyToIdx)) {
        // 若不存在该映射则去初始化映射
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    }
    // 若newStartVnode存在key的情况,则去映射中查找,若无则从oldStartIdx到oldEndIdx遍历after列表查找新节点是否存在
    idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    // 若新节点不存在于旧节点数组中,新建一个元素并插入真实DOM节点列表中
    if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    } else {
        // 若在旧列表中查找到新节点,则去判断两个节点是否相等
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
            // 更新节点内容和哨兵并进行节点的移动
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
    }
    newStartVnode = newCh[++newStartIdx]
}
循环结束

最后当oldStartIdx > oldEndIdx || newStartIdx > newEndIdx,也就是新或旧节点数组有一个被查找完之后则退出判断循环。当循环结束时,旧节点数组中剩下的节点即为要删除的节点,新节点数组中剩下的即为要新增的节点。只需要进行简单的新增和删除操作即可,代码如下:

if (oldStartIdx > oldEndIdx) {
    // 新节点数组中有剩余,新增新节点
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
    // 旧节点数组中有剩余,删除旧节点
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}

经历过了这么多的判断之后,就完成了同级节点之间的Diff比较。

就地复用

在Diff中会使用到一种就地复用的策略。就地复用是指Vue会尽可能复用之前的DOM,尽可能不发生DOM的移动。

Vue判断新旧节点是否为相同节点(也就是上面的sameVnode方法),这个相同节点的意思并不是两个完全相同的节点,实际上它仅判断是否为同类节点(同类节点为类型相同且节点数据一致,如前后两个span,span标签上的属性没有改变,但是里面的内容变了,这样就算作同类节点),如果是同类节点,那么Vue会直接复用旧DOM节点,只要更新节点中的内容即可。这样可以大大减少列表中节点的移动操作。

图解Diff

下面通过之前的初始化的节点图,进行一步一步的图解。

(1)在初始化并设置了哨兵之后,进入了条件判断循环。第一步发现了旧数组的头和新数组的尾都是A节点,这时候进入了上面的判断5oldStartIdx向后移动,newEndIdx向前移动。更新A节点内容并在真实DOM中将A移动到队伍最后
第二步

(2)第二次循环,进入判断7,发现新节点E并不存在于旧节点列表中,只能新建E节点,并插入真实DOM中。哨兵newStartIdx向后移动。
第二步

(3)第三次循环,进入判断7,根据key map获取遍历旧节点数组发现C节点存在旧节点数组中,获取C节点在旧节点数组中的位置,在真实DOM中将C节点插入到oldStartNode(B节点)前面,将旧节点数组中的该元素(before[idxInOld])置为undefined,newStartIdx向后移动。
第三步

(4)第四次循环,同第二次循环,新节点F并不存在旧节点数组中,新建F节点,并插入节点C后。newStartIdx向后移动。
在这里插入图片描述
(5)newStartIdx > newEndIdx,不满足循环条件,即新节点数组已处理完成。接下来进入退出循环后的条件处理,所以从oldStartIdx到oldEndIdx遍历旧节点数组,依次删除B,D两个节点。完成节点比较

在这里插入图片描述

总结

Vue中的Diff算法采用了React相似的思路,都是同层节点进行比较,在比较的过程中,使用了一些优先判断和就地复用策略,提高了Diff算法的效率。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Vue中的Diff算法 的相关文章

随机推荐

  • 停止Tomcat报错:java.net.ConnectException: 拒绝连接 (Connection refused)

    问题描述 今天在部署项目时 发现停止tomcat的过程中抛出了异常 java net ConnectException 拒绝连接 Connection refused 几次尝试 项目中写的有定时任务 所以猜测是定时任务导致项目停止失败 解决
  • java操作excel表格详解

    在日常工作中 对Excel工作表格的操作处理可是多的数不清楚 下面是java语言对其的操作 有需要的小伙伴可以参考下 使用场景 1 将用户信息导出为excel表格 导出数据 2 将Excel表中的信息录入到网站数据库 习题上传 大大减轻网站
  • 【爬虫+可视化】Python爬取疫情数据,并做可视化展示

    知识点 爬虫基本流程 json requests 爬虫当中 发送网络请求 pandas 表格处理 保存数据 pyecharts 可视化 开发环境 python 3 8 比较稳定版本 解释器发行版 anaconda jupyter noteb
  • MPI 和OPENMP 混合编程 实现矩阵LU分解

    LU分解 将系数矩阵A转变成等价两个矩阵L和U的乘积 其中L和U分别是下三角和上三角矩阵 当A的所有顺序主子式都不为0时 矩阵A可以分解为A LU 且分解唯一 其中L是单位下三角矩阵 U是上三角矩阵 方法 使用openMP和MPI混合编程现
  • Jeesite开发平台限制用户多点登录

    Jeesite开发平台限制用户多点登录 授权查询回调函数 进行鉴权但缓存中无用户的授权信息时调用 Override protected AuthorizationInfo doGetAuthorizationInfo PrincipalCo
  • web前端性能优化

    1 图片处理 图片压缩 使用图片图片压缩 优化工具TinyPNG TinyJPG压缩图片 或者使用其Gulp 组件gulp tinypng结合到自动化构件流程中 图片格式转为base64 使用webpack的url loader 自动根据文
  • MyBatis 的架构

    MyBatis 的架构 MyBatis 是一个基于 Java 的持久层框架 可以将 SQL 语句和 Java 代码进行分离 通过 XML 或注解的方式配置 SQL 语句并执行 从而实现数据访问的功能 MyBatis 的架构包括以下几个部分
  • Mysql 实战之——读写分离方案

    Linux环境 Centos 6 8 64 bit Mysql 版本 5 1 7 一 准备工作 部署Mysql主从复制 二 使用Amoeba数据库代理来实现读写分离 Amoeba作为数据库代理 以中间件的形式存在 拓扑图如下所示 Amoeb
  • cad多个窗口并排显示_CAD的入门小技巧

    在CAD中可以绘制二维 三维图形 也可以对图纸中的图形进行标注和进行渲染 比较广泛的应用于建筑 机械 环境工程 电子 设计等一些行业 启动与退出启动 1 在桌面双击CAD图标2 开始 程序 Autodesk Autodesk CAD CAD
  • got an unexpected keyword argument 'xxx'

    这几天在捣鼓pyecharts的地图功能
  • Windows Mobile 设备中心 for vista 一览

    2007年06月21日 14 30 00 Microsoft Windows Mobile 设备中心 6 1 在6月6日发布了最新版 今天为了能在Vista开发PPC 或Wince设备 程序 下载安装了该程序 启动后界面确实很炫 和媒体中心
  • 【论文合集】2022年11月医学影像期刊论文合集

    本月IEEE Transactions on Medical Imaging 1区 top if 11 037 共41篇 Medical Image Analysis 1区 top if 13 828 共47篇 标题高频词汇 segment
  • Non-terminating decimal expansion; no exact representable decimal result.

    日志信息 问题分析 由于 BigDecimal 是不可变的 任意精度的有符号十进制数 所以可以做精确计算 但在除法中 准确的商可能是一个无限长的十进制扩展 例如 1 除以 3 所得的商 若我们在做除法时 没有指定舍入模式 无法表示为准确的结
  • C# 串口通信 stm32 电机

    前几天已经完成了stm32通过PWM对电机的控制 这几天趁上班之余 也完成了c 通过串口通信控制电机的运行 界面如下 好久没写文章了 发现非常不擅长分享和表达 第一反应是演示出来 可惜这里不能有动画 功能不强大啊 哪天有空了 把上位机代码和
  • json库 nlohmann/json 的基本使用

    C 的json库有很多 但nlohmann 链接 https github com nlohmann json 大概是目前使用最方便的跨平台json库了 其可以让用户以modern C 的方式解析和构建json 性能比rapidjson库略
  • 安卓c语言获取context,Android中Context详解 ---- 你所不知道的Context

    二 什么时候创建Context实例 熟悉了Context的继承关系后 我们接下来分析应用程序在什么情况需要创建Context对象的 应用程序创建Context实例的 情况有如下几种情况 1 创建Application 对象时 而且整个App
  • 一点就通——ChatGPT翻译润色的最新简明使用方案

    prompt使用推荐 1 翻译prompt 翻译主要有两种 第一种是我们的老朋友厦门大学潘王雨昂 个人主页 pwya github io 所编写使用的prompt 第二种是我自己改造的 1 我希望你能担任英语翻译 拼写校对和修辞改进的角色
  • JAVA——JSch

    第 1 章 JSch简介 1 1 简述 1 jsch是ssh2的一个纯Java实现 它允许你连接到一个sshd服务器 使用端口转发 X11转发 文件传输等 2 SSH 是较可靠 专为远程登录会话和其他网络服务提供安全性的协议 3 ftp协议
  • 金融量化— 动量策略(Momentum Strategy)

    什么是动量效应和动量交易策略 动量效应是指过去收益较高的资产 在未来一段时间内仍获得较高的收益 过去收益较低的资产在未来仍获得较低的收益 对于动量效应现象的解释 传统金融学认为 动量效应的存在并不是市场无效的证据 并试图从理性风险补偿这一角
  • Vue中的Diff算法

    Vue中的Diff算法 本篇文章主要介绍Diff算法的思想和Vue中对Diff算法的基本实现 1 为什么要用Diff算法 由于在浏览器中操作DOM的代价是非常 昂贵 的 所以才在Vue引入了Virtual DOM Virtual DOM是对