说说vue的diff算法

  • Vue的diff算法
    • 是什么
    • 比较方式 – 深度优先,同层比较
      • 比较只会在同层级进行,不会跨层级比较
      • 比较的过程中,循环从两边向中间收拢
    • diff 算法更新的例子
    • 原理分析
      • patch
      • patchVnode
      • updateChildren
    • 小结

Vue的diff算法

此文章,来源于印客学院的资料,这里只是分享,便于查漏补缺。

Vue的diff算法是用于 比较新旧虚拟DOM树的差异,并将这些差异应用到实际的DOM上,以实现高效的更新和渲染

Vue的diff算法采用了一种双端比较的策略,即同时从新旧虚拟DOM的头部和尾部开始进行比较。这种策略可以在某些情况下更快地找到差异。

具体的diff算法步骤如下:

  1. 首先,进行根节点的比较。如果新旧虚拟DOM的根节点不同,那么直接替换整个节点及其子节点。

  2. 如果根节点相同,则进一步比较它们的子节点。Vue使用了key属性来标识节点的唯一性,以便更快地找到对应关系。如果新旧虚拟DOM的子节点列表不同,Vue会通过一些启发式的方法尽可能地复用已存在的真实DOM节点,而不是重新创建和删除节点。

  3. 在比较子节点时,Vue会根据一定的规则进行精细化的差异计算。这些规则包括同层级节点的移动、插入和删除等操作,以及跨层级节点的移动和删除等操作。Vue会尽量将这些操作合并,减少对实际DOM的操作次数。

  4. 在进行差异计算时,Vue会使用一些优化策略,例如使用双指针算法、按需更新等。这些策略可以帮助Vue更快地找到差异并减少不必要的操作。

Vue的diff算法通过高效地比较虚拟DOM的变化,最小化实际DOM的操作,以提高性能和渲染效率。但需要注意的是,diff算法并非完美的,它也有一些局限性,特别是在处理大型列表或具有复杂嵌套结构的组件时可能会存在性能问题。在这些情况下,可以考虑使用一些优化手段,如使用key属性来提高diff算法的效率,或者采用其他更高级的优化策略。

更多详细内容,请微信搜索“前端爱好者戳我 查看

是什么

diff 算法是一种 通过同层的树节点 进行比较的高效算法其有两个特点:

  • 比较只会在同层级进行,不会跨层级比较
  • 在diff比较的过程中,循环从两边向中间

比较diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较

比较方式 – 深度优先,同层比较

diff 整体策略为: 深度优先,同层比较

比较只会在同层级进行,不会跨层级比较

比较的过程中,循环从两边向中间收拢

diff 算法更新的例子

下面举个 vue 通过 diff 算法更新的例子

新日 VNode 节点如下图所示

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

第二次循环后,同样是旧节点的未尾和新节点的开头(都是 C)相同,同理, diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 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 节点后面

原理分析

patch

当数据发生改变时, 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 = falseconst insertedVnodeQueue = []if (isUndef(oldVnode)) {isInitialPatch = truecreateElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素 } else {const isRealElement = isDef(oldVnode.nodeType)if (!isRealElement && sameVnode(oldVnode, vnode)) {// 判断旧节点和新节点自身一样,一致执行patchVnodepatchVnode(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}}
}

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

  • 没有新节点,直接触发旧节点的 destory 钩子
  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode 去处理这两个节点
  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点
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.componentInstancereturn}let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}const oldCh = oldVnode.childrenconst ch = vnode.childrenif (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)) {// 并且子节点不完全一致,则调用updateChildrenif (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)}
}

patchVnode 主要做了几个判断:

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

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

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, rem
oveOnly) {let oldStartIdx = 0 // 旧头索引let newStartIdx = 0 // 新头索引let oldEndIdx = oldCh.length - 1 // 旧尾索引let newEndIdx = newCh.length - 1 // 新尾索引let oldStartVnode = oldCh[0] // oldVnode的第一个 childlet oldEndVnode = oldCh[oldEndIdx] // oldVnode 的最后一个 childlet newStartVnode = newCh[0] // newVnode 的第一个childlet newEndVnode = newCh[newEndIdx] // newVnode的 最后一个 childlet 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 transitionsconst 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, 索引左移,继续循环epatchVnode(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)// 如果remove0nly是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// 创建一个新VnodecreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)// 如果找到了和newStartVnodei具有相同的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 vnodeToMovenewStartVnode patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)// 清除oldCh[idxInOld] = undefined// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm // 移动到oldstartVnode. canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)// 如果key相同,但是节点不相同,则创建一个新的节点 } else {// same key but different element. treat as new elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)}}// 右移newStartVnode = newCh[++newStartIdx]}
}   

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

  • 当新老 VNode 节点的 start 相同时,直接 patchVnode同时新老 VNode 节点的开始索引都加 1
  • 当新老 VNode 节点的 end 相同时,同样直接 patchVnode ,同时新老 VNode 节点的结束索引都减 1
  • 当老 VNode 节点的 start 和新 VNode 节点的 end 相同时,这时候在 patchVnode后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 Vode 节点开始索引加 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 的位置0

小结

  • 当数据发生改变时,订阅者 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.hqwc.cn/news/455369.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Could not connect to Redis at 127.0.0.1:6379:由于目标计算机积极拒绝,无法连接...问题解决方法之一

一、问题描述 将Redis压缩包解压后&#xff0c;安装Redis过程中出现问题Could not connect to Redis at 127.0.0.1:6379:由于目标计算机积极拒绝&#xff0c;无法连接... 官网windows下redis开机自启动的指令如下&#xff1a; 1、在redis目录下执行 redis-server --service-in…

Java面向对象 创建类 创建对象

目录 创建类类的属性类的方法实例分析 创建对象创建Test类测试分析 创建类 类的属性 属性用于定义该类或该类对象包含的数据或者说静态特征。属性作用范围是整个类体。 属性定义格式&#xff1a; [修饰符] 属性类型 属性名 [默认值] ;类的方法 方法用于定义该类或该类实例…

释放资源的方式

try - catch - finally finally代码区的特点&#xff1a;无论try中的程序是正常执行力&#xff0c;还是出现了异常&#xff0c;最后都一定会执行finally区&#xff0c;除非JVM终止。 作用&#xff1a;一般用于在程序执行完成后进行资源的释放操作&#xff08;专业级做法&#x…

用 Delphi 程序调用 Python 代码画曲线图

用 Python 的库画图 Python 代码如下&#xff1a; import matplotlib.pyplot as pltsquares [1, 4, 9, 16, 25]; plt.plot(squares); plt.grid(True) # 网格线 plt.show(); # 这句话会弹出个窗口出来&#xff0c;里面是上述数据的曲线。 把以上代码&#xff0c;放进 PyS…

对于模糊查询的SQL,怎么优先返回等值记录

说明&#xff1a;记录一次SQL改进的方法&#xff0c;希望能对大家有启发。 场景 前端项目有一个输入框&#xff0c;根据输入的银行名称&#xff0c;去模糊查询对应的数据库表&#xff0c;返回结果集&#xff0c;显示到下拉列表中。 因为银行名称字段包括了分行名&#xff0c…

如何进行游戏服务器的负载均衡和扩展性设计?

​在进行游戏服务器的负载均衡和扩展性设计时&#xff0c;需要考虑多个方面&#xff0c;以确保服务器的稳定性和可扩展性。以下是一些关键的步骤和考虑因素&#xff1a; 负载均衡的需求分析 在进行负载均衡设计之前&#xff0c;需要深入了解游戏服务器的负载特性和需求。这包括…

DevOps落地笔记-15|混沌工程:通过问题注入提高系统可靠性

上一课时介绍了通过搭建一套部署流水线&#xff0c;高效、可靠的将软件部署到测试环境以及生产环境。到目前为止&#xff0c;我们学习了从用户需求到软件部署到生产环境交付给用户的全过程。随着软件工程不断发展&#xff0c;近几年&#xff0c;出现了一种新的实践&#xff0c;…

idea(2023.3.3 ) spring boot热部署,修改热部署延迟时间

1、添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional> </dependency>载入依赖 2、设置编辑器 设置两个选项 设置热部署更新延迟时…

无向图-树的重心-DFS求解

思路&#xff1a; 本题的本质是树的dfs&#xff0c; 每次dfs可以确定以u为重心的最大连通块的节点数&#xff0c;并且更新一下ans。 也就是说&#xff0c;dfs并不直接返回答案&#xff0c;而是在每次更新中迭代一次答案。 这样的套路会经常用到&#xff0c;在 树的dfs 题目中…

python的内置函数-print()、input()、range()

内置函数 一、print()二、input()三、range()range的定义与特点range()函数的使用使用range()创建数字列表 一、print() print()是一个内置函数&#xff0c;用于将指定的内容打印到控制台。 #基本用法&#xff1a; print(value1, ..., sep , end\n, filesys.stdout, flushFal…

双向链表的插入、删除、按位置增删改查、栈和队列区别、什么是内存泄漏

2024年2月4日 1.请编程实现双向链表的头插&#xff0c;头删、尾插、尾删 头文件&#xff1a; #ifndef __HEAD_H__ #define __HEAD_H__ #include<stdio.h> #include<stdlib.h> #include<string.h> typedef int datatype; enum{FALSE-1,SUCCSE}; typedef str…

npm淘宝镜像源换新地址

新的淘宝npm镜像源地址&#xff1a;https://registry.npmmirror.com 切换新的镜像源 npm config set registry https://registry.npmmirror.com然后再执行以下操作查看是否成功 npm config list如果没安装过淘宝镜像源的&#xff0c;则直接安装 npm install -g cnpm --regi…