关于Vue中Diff算法详解

news2024/11/13 10:15:38

一、(Diff)是什么?

diff 算法是一种通过同层的树节点进行比较的高效算法

1.1. 特点:

  • 比较只会在同层级进行, 不会跨层级比较
  • 在diff比较的过程中,循环从两边向中间比较
  • diff 算法的在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较

二、比较方式

2.1. diff整体策略为:深度优先,同层比较,比较只会在同层级进行, 不会跨层级比较
在这里插入图片描述

2.2. 比较的过程中,循环从两边向中间收拢
在这里插入图片描述
2.3. 举例: 新旧VNode节点如下图所示:
预览

  • 第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff后的第一个真实节点,同时旧节点endIndex移动到C,新节点的 startIndex 移动到了 C
    预览

  • 第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 B 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E
    预览

  • 第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不动
    预览

  • 第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的 startIndex 移动到了 B
    在这里插入图片描述

  • 第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex 移动到了 C,新节点的 startIndex 移动到了 F
    预览

  • 第六次新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdx 和 newEndIdx 之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面
    预览

三、原理分析

当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图

源码位置:src/core/vdom/patch.js

function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
    } else {
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // 判断旧节点和新节点自身一样,一致执行patchVnode
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        } else {
            // 否则直接销毁及旧节点,根据新节点生成dom元素
            if (isRealElement) {

                if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
                    oldVnode.removeAttribute(SSR_ATTR)
                    hydrating = true
                }
                if (isTrue(hydrating)) {
                    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
                        invokeInsertHook(vnode, insertedVnodeQueue, true)
                        return oldVnode
                    }
                }
                oldVnode = emptyNodeAt(oldVnode)
            }
            return vnode.elm
        }
    }
}

3.1. patch函数前两个参数位为oldVnode 和 Vnode ,分别代表新的节点和之前的旧节点,主要做了四个判断:

  • 没有新节点,直接触发旧节点的destory钩子
  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode 去处理这两个节点
  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点
    下面主要讲的是patchVnode部分
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 如果新旧节点一致,什么都不做
    if (oldVnode === vnode) {
      return
    }

    // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
    const elm = vnode.elm = oldVnode.elm

    // 异步占位符
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // 如果新旧都是静态节点,并且具有相同的key
    // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
    // 也不用再有其他操作
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 如果vnode不是文本节点或者注释节点
    if (isUndef(vnode.text)) {
      // 并且都有子节点
      if (isDef(oldCh) && isDef(ch)) {
        // 并且子节点不完全一致,则调用updateChildren
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

        // 如果只有新的vnode有子节点
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // elm已经引用了老的dom节点,在老的dom节点上添加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

        // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)

        // 如果老节点是文本节点
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }

      // 如果新vnode和老vnode是文本节点或注释节点
      // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

3.2. patchVnode主要做了四个判断:

  • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容
  • 新节点和旧节点如果都有子节点,则处理比较更新子节点
  • 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点
  • 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除

3.3. 子节点不完全一致,则调用updateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 旧头索引
    let newStartIdx = 0 // 新头索引
    let oldEndIdx = oldCh.length - 1 // 旧尾索引
    let newEndIdx = newCh.length - 1 // 新尾索引
    let oldStartVnode = oldCh[0] // oldVnode的第一个child
    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
    let newStartVnode = newCh[0] // newVnode的第一个child
    let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果oldVnode的第一个child不存在
      if (isUndef(oldStartVnode)) {
        // oldStart索引右移
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

      // 如果oldVnode的最后一个child不存在
      } else if (isUndef(oldEndVnode)) {
        // oldEnd索引左移
        oldEndVnode = oldCh[--oldEndIdx]

      // oldStartVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // patch oldStartVnode和newStartVnode, 索引左移,继续循环
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

      // oldEndVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // patch oldEndVnode和newEndVnode,索引右移,继续循环
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

      // oldStartVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // patch oldStartVnode和newEndVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // oldStart索引右移,newEnd索引左移
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]

      // 如果oldEndVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // patch oldEndVnode和newStartVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // oldEnd索引左移,newStart索引右移
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]

      // 如果都不匹配
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        // 如果未找到,说明newStartVnode是一个新的节点
        if (isUndef(idxInOld)) { // New element
          // 创建一个新Vnode
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

        // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
        } else {
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }

          // 比较两个具有相同的key的新节点是否是同一个节点
          //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // patch vnodeToMove和newStartVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 清除
            oldCh[idxInOld] = undefined
            // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
            // 移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)

          // 如果key相同,但是节点不相同,则创建一个新的节点
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          }
        }

        // 右移
        newStartVnode = newCh[++newStartIdx]
      }
    }

3.4. while循环主要处理了以下五种情景:

  • 当新老 VNode 节点的 start 相同时,直接 patchVnode ,同时新老 VNode 节点的开始索引都加 1

  • 当新老 VNode 节点的 end相同时,同样直接 patchVnode ,同时新老 VNode 节点的结束索引都减 1

  • 当老 VNode 节点的 start 和新 VNode 节点的 end 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1

  • 当老 VNode 节点的 end 和新 VNode 节点的 start 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减 1,新 VNode 节点的开始索引加 1

  • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:

    • 从旧的 VNode 为 key 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode ,同时将这个真实 dom 移动到 oldStartVnode 对应的真实 dom 的前面
    • 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置

总结

  • 当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补丁
  • 通过isSameVnode进行判断,相同则调用patchVnode方法
    patchVnode做了以下操作:
    • 找到对应的真实dom,称为el
    • 如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节点
    • 如果oldVnode有子节点而VNode没有,则删除el子节点
    • 如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el
    • 如果两者都有子节点,则执行updateChildren函数比较子节点
  • updateChildren主要做了以下操作:
    • 设置新旧VNode的头尾指针
    • 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode 节点再分情况操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/189126.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DAMA数据管理知识体系指南之数据治理

第3章 数据治理 3.1 简介 数据治理是对数据资产管理行使权力和控制的活动集合(规划、监控和执行&#xff09;。数据治理职能指导其他数据管理职能如何执行。数据治理是在高层次上执行数据管理制度。 3.2 概念和活动 有效的数据管理&#xff0c;特别是在数据治理方面&#…

2023最新前端面试题4(持续更新)

JavaScript 59、JS的几条基本规范 1、不要在同一行声明多个变量 2、请使用/&#xff01;来比较true/false或者数值 3、使用对象字面量替代new Array这种形式 4、不要使用全局变量 5、Switch语句必须带有default分支 6、函数不应该有时候有返回值&#xff0c;有时候没有返回值 …

如何写出有效的单元测试?

如何写出有效的单元测试&#xff1f; 目录&#xff1a;导读 什么是单元测试 为什么需要单元测试 什么是有效的单元测试 为什么有效的单元测试如此重要 如何写有效的单元测试 补充单元测试应该从哪里开始 可测试的设计 单元测试与重构 做不到TDD&#xff0c;可以做到测…

LaoCat带你认识容器与镜像(四【下】)

基础概念章节是告一段落了&#xff0c;不知道各位童鞋是否阅读的顺畅&#xff0c;欢迎各位童鞋踊跃的提出意见和看法并指正。 本章内容 Dockerfile基础命令详解。 本文实操全部基于Ubuntu 20.04 宿主机 > linux服务器本身 上节粗略介绍了Dockerfile相关命令&#xff0c;这节…

国电投-风机叶片开裂故障预警比赛(记录)

1 前言 1-1 简介 DataFountain平台举办的比赛&#xff0c;赛题&#xff1a;风机叶片开裂故障预警。以下是比赛链接:风机叶片开裂故障预警 Competitions - DataFountain 1-2 任务背景 SCADA是风场设备管理、监测、和控制的重要系统&#xff0c;通过实时收集风机运行的环境参数、…

oracle direct path read等待事件处理案例

一 问题描述 收到短信告警&#xff0c;提示direct path read(110)。 直接路径读取不过SGA缓存&#xff0c;直接从磁盘上读数据&#xff0c;每次查询都会产生大量的物理读&#xff0c;导致IO比较高&#xff0c;影响数据库性能。 二 排查思路 生成AWR报告及ASH报告&#xff0…

springboot 整合mybatis-plus的自动生成代码包含service和controller

版本信息springboot&#xff1a;<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.13.RELEASE</version></parent>mybatis相关依赖版本如下&#xff1a…

压缩包文件上传导致覆盖rce

制作恶意压缩包 tgao.jsp文件内容&#xff1a; <html> <body><%out.println("zip slip getshell.");%> </body> </html>编写python脚本将jsp文件内容内容压缩至DocSystem.war中&#xff0c;并指定name为../../DocSystem/tgao.jsp&am…

ElasticSearch - RestClient查询文档

目录 查询文档-快速入门 查询文档-match查询 查询文档-精确查询 查询文档-布尔查询 查询文档-排序和分页 查询文档-高亮显示 查询文档-快速入门 文档的查询基本步骤包括&#xff1a; (1)准备Request对象(2)准备请求参数&#xff0c;构建查询条件(3)发起请求&#xff0c;得…

OpenSSL下载安装教程

OpenSSL下载 &#xff08;加急&#xff09;下载地址&#xff1a;Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions 首先&#xff0c;进入官网 Shining Light Productions - Home &#xff0c;可以看到如下界面&#xff1a; 这里演示用的版本是V1.1的版…

nodejs+vue考研信息查询系统vscode项目

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 2 第3章 系统分析 3 3.1 需求分析 3 3.2 系统可行性分析 4 3.2.1技术可行性&#xff1a;技术背景 4 3.2.2经…

2023蓝桥杯学习与刷题建议

前两天天给你们组了队&#xff0c;经过两天发现各位都有这样的问题&#xff1a; 不愿意交流。小组不会规划刷题计划。可能是因为没有人带头和具体刷题计划&#xff0c;所以都处于迷茫&#xff0c;不交流、不刷题。还有可能是大家都不认识&#xff0c;都比较羞涩。同时也有我个…

OSCP_VULHUB_DC: 2

文章目录前言信息收集Web账号密码爆破ssh登录绕过rbash限制git提权前言 kali攻击&#xff1a;192.168.132.139 目标主机&#xff1a;192.168.132.146 环境配置参照https://www.vulnhub.com/entry/dc-2,311/ 下载&#xff1a;https://download.vulnhub.com/dc/DC-2.zip 信息收…

开发第四天+第五天读书笔记

首先用C语言实现内存写入: 光是成功的让画面黑屏是不够的&#xff0c;还是要往画面上画点什么。首先修改naskfunc.nas。写成这样&#xff1a; ; naskfunc ; TAB4[FORMAT "WCOFF"] ; 创建对象文件的模式 [INSTRSET "i486p"] …

Linux进程间通信(基于管道通信)

目录 一、进程间通信 1.1 通信目的 1.2 通信发展 二、管道 2.1 管道的概念和分类 2.2 匿名管道 2.2.1 匿名管道&#xff08;基于父子血缘关系) 2.2.2 匿名管道单向性 2.2.3 匿名管道是内存级别文件 2.2.4 匿名管道指令实现 2.2.5 代码实现匿名管道&#xff08;pipe()…

一文讲清楚shell 中的变量是怎么回事

目录 变量和引用 一&#xff0c; 什么是变量 二&#xff0c; 根据数据类型分类 三&#xff0c; 根据作用域分类 1、环境变量 2、普通变量 四&#xff0c; 变量的定义 1、变量定义示例&#xff1a;变量名变量值 2、位置参数和预定义变量 五&#xff0c; shell中的引用 六&…

【GPLT 二阶题目集】L2-022 重排链表

输入格式&#xff1a; 每个输入包含1个测试用例。每个测试用例第1行给出第1个结点的地址和结点总个数&#xff0c;即正整数N (≤10^5 )。结点的地址是5位非负整数&#xff0c;NULL地址用−1表示。 接下来有N行&#xff0c;每行格式为&#xff1a; Address Data Next 其中Addres…

深拷贝和浅拷贝对比

JavaScript存储引用数据&#xff08;对象&#xff09;都是存地址的&#xff0c;存放在堆内存中的对象&#xff0c;在栈内存中存的是一个指针&#xff0c;这个指针指向堆内存一个位置。再从堆内存中取得所需的数据。 深拷贝&#xff1a;对数据进行拷贝之后&#xff0c;修改拷贝之…

Python:从协议到抽象基类

本章话题是接口&#xff1a;鸭子类型代表特征动态协议&#xff1b; 使接口更明确、能验证实现是否副了规定的抽象基类ABC(Abstact Base Class).Python语言诞生15年后&#xff0c;Python2.6中才引入了抽象基类&#xff0c;抽象基类。对于java、C#类似的语言&#xff0c;会觉得鸭…

DevSecOps敏捷安全技术金字塔V3.0正式发布

2022年12月28日&#xff0c;由悬镜安全主办&#xff0c;3S-Lab软件供应链安全实验室、Linux基金会OpenChain社区、ISC、OpenSCA社区联合协办的第二届全球DevSecOps敏捷安全大会&#xff08;DSO 2022&#xff09;已通过全球直播的形式圆满举行。本届大会以“共生敏捷进化”为主题…