vue源码解析——diff算法/双端比对/patchFlag/最长递增子序列

news2024/11/16 9:27:46

虚拟dom——virtual dom,提供一种简单js对象去代替复杂的 dom 对象,从而优化 dom 操作。virtual dom 是“解决过多的操作 dom 影响性能”的一种解决方案。virtual dom 很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达到平衡。

diff 算法是一种优化手段,将前后两个模块进行差异化比较,修补(更新)差异的过程叫做 patch,也叫打补丁。只有当新旧子节点的类型都是多个子节点时,核心 Diff 算法才派得上用场。diff的目的是时间换空间:尽可能通过移动旧节点,复用旧节点DOM元素,减少新增DOM操作。通过首首、尾尾、首尾、尾首以及在旧节点列表遍历等方式逐个试探去找可复用的旧节点。

vue3引入了最长递增子序列优化diff:去掉相同的前缀和后缀,也就是首首、尾尾都比较完后剩余的旧节点列表和新节点列表进行diff。在新节点列表中用一个数组,统计新节点出现在旧节点相同元素的index;对这个数组求最长递增子序列。递增子序列的节点不需要移动(即使不连续),因为它们在新旧节点序列中的相对位置是一样的。

diff算法目的

在页面数据发生变化,进行组件更新的时候,产生了新节点虚拟dom。这个时候需要将新节点的dom渲染为真实的dom。核心diff算法就是在已知旧节点的DOM结构、旧节点vnode和新子节点的vnode情况下,以较低的成本完成子节点的更新为目的,求解生成子节点dom的系列操作。

如果我将新的虚拟dom转为真实dom,这个过程消耗性能。如果能复用老节点的dom节点,是不是可以减少dom操作啊。

那对于新旧两个dom树而言,如何比较呢?

vue采用的是深度递归+同层比较。深度递归能保证每个节点都能遍历到,同层比较能够缩小比较范围。

 diff算法比较流程

比较是否是相同节点;相同节点比较属性,并复用老节点;

如果是相同节点,考虑老节点和新节点的儿子节点情况:

  • 老的没有儿子,新的有儿子,将新的儿子节点挂载给老节点;
  • 老的有儿子,新的没儿子。删除页面节点;
  • 老的有儿子,新的有儿子,但都是文本节点,直接更新文本节点;
  • 老的儿子是一个列表,新的儿子也是一个列表。核心diff,双端比较。

vue双端比较

所谓双端比较就是新列表旧列表两个列表的头与尾互相对比,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。

 思考:为什么需要头头、尾尾、头尾、尾头四种比较?

在看diff算法是时候一定要有一个目标——我要尽可能在旧节点列表中找到一个可以复用的节点!这样就可以复用老节点的数据,而避免了新增dom。为什么使用这四种方式?考虑了下面新旧节点列表可能排列的情况:元素的追加、删除、向左翻转、向右翻转、逆序排列等方式

 diff的整个优化采用了双指针的方式,在新老节点列表的头尾插了两个指针。在比较的过程中如果新老节点有一方头尾指针重合了,意味着节点遍历结束。这个时候需要终止diff算法。在比较过程中,如果头指针新老节点相等,头指针向后移动,头指针++;如果尾指针相等,尾指针向前移动,尾指针--。

 头头比较

看下下面这种场景,尾部插入数据。仅使用头和头比较,就能找到复用的节点,且不需要移动旧节点。如果新节点有多余元素,将新节点中多余部分插入到dom里。如果老节点有多余元素,删除老节点多余节点。

 

尾尾比较

头部插入数据,只需要尾尾进行比较。当某一方头尾指针重合了结束。此时如果新节点元素多,将多余的元素一起新增dom操作。其余复用老节点。如果老节点多,同理,将多余的老节点删除

尾头比较

如果头和头,尾和尾都比较不成功,还是尝试从两端找相同节点。此时是不是可以交叉比较。这里举一个尾头比较的例子:

旧节点尾指针指向的D和新节点头指针指向的D相等。此时D是不是要找相对位置,要与新节点保持一致,那么就移动旧节点D到旧节点的头部。保持列表顺序一致。

li-d 节点所对应的真实 DOM 原本是最后一个子节点,并且更新之后它应该变成第一个子节点。所以我们需要把 li-d 所对应的真实 DOM 移动到最前方即可 。

由于 li-d 节点所对应的真实 DOM 元素已经更新完成且被移动,所以现在真实 DOM 的顺序是:li-dli-ali-bli-c,如下图所示: 

 

头尾比较

如果前面三种都找不到相同节点,看头尾能否找到。在下面的例子中旧节点的头和新节点的尾相等。此时移动旧节点的A到旧节点尾部,与新节点的相对位置保持一致。之后头指针向后++,尾指针向前--。

 

双端非理想乱序——key映射表查找老节点

在之前的讲解中,我们所采用的是较理想的例子,换句话说,在每一轮的比对过程中,总会满足四个步骤中的一步,但实际上大多数情况下并不会这么理想,如下图所示:

 

可以根据节点的key建立一个旧节点key的映射表。然后用新节点头指针newStartVnode遍历新节点,根据新节点从映射表里找旧节点,如果找到了就复用,找不到就创建。复用老节点,同时移动老节点,这个时候节点在列表中间,只需要 将找到的元素,比如上图将b移动到a前面,同时原有的b要设为undefined,因为要保留占位。处理完,新节点头指针++

 

考虑过程中出现节点会变成为undefined,所以找旧节点时要加判断,如果是undefined就跳过 

 新老节点列表长度不相等

如果新老节点有一方长度不相等;循环结束后,肯定有一方多余。这个时候要判断是新节点多了还是老节点多了。多余的节点在startIndex和endIndex中间,所以判断哪个的startIndex>oldIndex。如果老节点多了就删除,如果新节点多了就插入。

diff结束条件

newStartIdx > newEndIdx 保证新节点遍历结束

 长度及序列相等

旧节点有多余

 diff伪代码

function vue2diff(prevChildren, nextChildren, parent) {
  let oldStartIndex = 0,
    newStartIndex = 0,
    oldStartIndex = prevChildren.length - 1,
    newStartIndex = nextChildren.length - 1,
    oldStartNode = prevChildren[oldStartIndex],
    oldEndNode = prevChildren[oldStartIndex],
    newStartNode = nextChildren[newStartIndex],
    newEndNode = nextChildren[newStartIndex];
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    if (oldStartNode === undefined) {
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefined) {
      oldEndNode = prevChildren[--oldStartIndex]
    } else if (oldStartNode.key === newStartNode.key) {
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
      patch(oldEndNode, newEndNode, parent)

      oldStartIndex--
      newStartIndex--
      oldEndNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newStartIndex]
    } else if (oldStartNode.key === newEndNode.key) {
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newStartIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newStartNode.key) {
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldStartIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
      if (oldIndex === -1) {
        mount(newStartNode, parent, oldStartNode.el)
      } else {
        let prevNode = prevChildren[oldIndex]
        patch(prevNode, newStartNode, parent)
        parent.insertBefore(prevNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      newStartIndex++
      newStartNode = nextChildren[newStartIndex]
    }
  }
  if (newStartIndex > newStartIndex) {
    while (oldStartIndex <= oldStartIndex) {
      if (!prevChildren[oldStartIndex]) {
        oldStartIndex++
        continue
      }
      parent.removeChild(prevChildren[oldStartIndex++].el)
    }
  } else if (oldStartIndex > oldStartIndex) {
    while (newStartIndex <= newStartIndex) {
      mount(nextChildren[newStartIndex++], parent, oldStartNode.el)
    }
  }
}

vue2源码分析

 源码地址vue-main\src\core\vdom\patch.ts

当数据发生改变时,订阅者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 节点再分情况操作

patch方法

patch函数前两个参数位为 oldVnodeVnode ,分别代表新的节点和之前的旧节点,主要做了四个判断:
  • 没有新节点,直接触发旧节点的destory钩子
  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode去处理这两个节点
  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点

 

核心当然还是新旧节点相同时的patchVnode阶段

patchVnode方法

 patchVnode方法,比较两个节点,并且比较两个节点的孩子节点

patchVnode主要做了几个判断:

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

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

updateChildren方法

新节点和旧节点是相同节点。比较新旧的子节点是否相等。如果新节点的chidren是个列表,同时旧节点的children也是个列表。此时调用updateChidren新旧的children的比较移动逻辑。这部分是核心diff

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
  • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
    • 从旧的 VNodekey 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode,同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面
    • 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置

 

vue3 diff算法做了哪些优化?

  1. 静态树提升(Static Tree Hoisting):Vue3使用静态树提升技术,将静态内容从动态内容中分离出来,并在渲染时只更新动态内容,从而减少不必要的更新操作,提高性能。

  2. PatchFlag:引入了PatchFlag标志,用于标识虚拟节点的类型和需要进行的操作,从而在更新过程中更快地识别和处理特定类型的更新。

  3. Fragments优化:针对不同类型的片段(如稳定片段、带键片段、无键片段等),Vue3采取不同的优化策略,以减少不必要的比较和更新操作。

  4. 动态插槽优化:针对具有动态插槽的组件,Vue3会进行特殊处理,并始终强制更新这些组件,以确保插槽内容正确渲染。

  5. 优化的算法逻辑:Vue3对diff算法的逻辑进行了优化,进行前缀后缀处理,并引入最长递增子序列,减少元素移动次数。使得在更新过程中能够更快地定位变化并进行更新,减少不必要的操作。

什么是PatchFlag?

vue3会根据元素是否有动态文本,动态样式,动态类等给不同的元素打上patchFlag标识。在diff算法比较的时候,会针对有patchFlag标识的节点进行比对。在Vue3中,patchFlag的值是一个整数,通过位运算来表示不同的更新操作类型。通过检查patchFlag的值,Vue3可以快速了解虚拟节点需要进行的具体操作,从而优化更新过程。


<div>
  <span>hello vue</span>
  <span>{{msg}}</span>
  <span :class="name">poetry</span>
  <span :id="name">poetry</span>
  <span :id="name">{{msg}}</span>
  <span :id="name" :msg="msg">poetry</span>
</div>

 可以看到vue3的模板编译,在动态的节点、样式、属性等节点都加上patchFlag编译后的值。对非动态的节点不加patchFlag。

vue2的模板编译就比较简单了

patchFlag的类型

 通过组合不同的补丁标志,可以在差异比较过程中针对特定类型的更新进行优化处理。有下面几种类型:

特殊标志:

  • TEXT:1,表示具有动态textContent(子节点快速路径)的元素。比如{{msg}}
  • CLASS:1<<1,表示具有动态类绑定的元素。比如“:class='colorStyle'”
  • STYLE:1<<2,表示具有动态样式的元素。编译器会将静态字符串样式预编译为静态对象,并检测并提升内联静态对象。例如,style="color: red":style="{ color: 'red' }"都会被提升为静态对象{ color: 'red' },以便在渲染函数中使用。

  • PROPS:1<<3,表示具有非类/样式动态属性的元素,或者是具有任何动态属性(包括类/样式)的组件。当存在这个标志时,虚拟节点还会有一个dynamicProps数组,其中包含可能发生变化的属性键,以便运行时可以更快地进行差异比较(无需担心已删除的属性)。

  • FULL_PROPS:1<<4,表示具有具有动态键的属性的元素。当键发生变化时,总是需要进行完整的差异比较以删除旧键。这个标志与CLASSSTYLEPROPS是互斥的。

  • NEED_HYDRATION:1<<2=5,表示需要对属性进行“hydration”(即初始化)的元素,但不一定需要进行补丁操作。例如,事件监听器和带有属性修饰符的v-bind

  • STABLE_FRAGMENT:1<<6,表示子节点顺序不会改变的片段。

  • KEYED_FRAGMENT:1<<7,表示具有带有key或部分带有key的子节点的片段。

  • UNKEYED_FRAGMENT:1<<8,表示具有无key子节点的片段。

  • NEED_PATCH:1<<9,表示只需要进行非属性补丁操作的元素,例如ref或指令(onVnodeXXX钩子)。由于每个已补丁的虚拟节点都会检查refonVnodeXXX钩子,因此它只是标记虚拟节点,以便父块可以跟踪它。

  • DYNAMIC_SLOTS:1<<10,表示具有动态插槽的组件(例如,引用v-for迭代值的插槽,或动态插槽名称)。具有此标志的组件始终会被强制更新。

  • DEV_ROOT_FRAGMENT:1<<11,表示仅因用户在模板的根级别放置了注释而创建的片段。这是一个仅用于开发环境的标志,因为在生产环境中会剥离注释。

  • HOISTED:-1,表示一个被提升的静态虚拟节点。这是一个提示,用于在“hydration”过程中跳过整个子树,因为静态内容永远不需要更新。

  • BAIL:-2,表示差异比较算法应该退出优化模式的特殊标志。例如,在由renderSlot()创建的块片段中遇到非编译器生成的插槽(即手动编写的渲染函数,应始终完全进行差异比较)时,或者手动克隆VNodes时。

什么是HoistStatic?

HoistStatic是Vue3中的一个特殊的patchFlag,值为-1。用于表示一个被提升的静态虚拟节点。当一个虚拟节点被标记为HoistStatic时,这意味着该节点是静态的,不需要进行更新操作,因为静态内容在渲染过程中永远不会改变。

通过将静态内容标记为HoistStatic,Vue3可以在“hydration”(即将虚拟DOM转换为真实DOM)过程中跳过整个子树的更新,从而节省时间和资源。这样可以提高渲染性能,特别是在处理大型组件树时。

 给定下面的代码,只有{{msg}}是动态的,会导致页面刷新,看下vue3的编译函数

<div>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>{{msg}}</span>
</div>

 可以看到静态节点的定义,被提升到父作用域,缓存起来。之后函数怎么执行,这些变量都不会重新定义一遍。

如果有多个静态节点呢?

当静态节点达到一定阈值后会被vue3合并起来

 

vue3的前置后置预处理+最长递增子序列

vue3diff借鉴于inferno (opens new window),该算法其中有两个理念。第一个是相同的前置与后置元素的预处理;第二个则是最长递增子序列,

前缀后缀预处理

如图所示,新旧 children 拥有相同的前缀节点和后缀节点

对于前缀节点,我们可以建立一个索引j,指向新旧 children 中的第一个节点,并逐步向后遍历,直到遇到两个拥有不同 key 值的节点为止 

我们需要处理的是相同的后缀节点,由于新旧 children 中节点的数量可能不同,所以我们需要两个索引prevEnd、nextEnd分别指向新旧 children 的最后一个节点,并逐步向前遍历,直到遇到两个拥有不同 key 值的节点为止 

 

理想:新节点多余

j > prevEnd 并且 j <= nextEnd

此时新节点列表还有节点

 新 children 中位于 jnextEnd 之间的所有节点都应该是新插入的节点

理想: 旧节点多余

j <= prevEnd并且 j > nextEnd

此时旧节点列表有多余节点

children 中有位于索引 jprevEnd 之间的节点,都应该被移除 

非理想:递增子序列

下面这个案例在预处理步骤之后,只有 li-a 节点和 li-e 节点能够被提前 patch。换句话说在这种情况下没有办法简单的通过预处理就能够结束 Diff 逻辑。这时我们就需要进行下一步操作

构建一个source数组

需要构造一个数组 source,该数组的长度等于新 children 在经过预处理之后剩余未处理节点的数量,并且该数组中每个元素的初始值为 -1。那么这个数组的作用是什么呢?该数组中的每一个元素分别与新 children 中剩余未处理的节点对应,实际上 source 数组将用来存储children 中的节点在旧 children 中的位置,后面将会使用它计算出一个最长递增子序列,并用于 DOM 移动

增加key-map映射表

新增一个映射表存储旧节点的key和node。用于 计算新 children 中的节点在旧 children 中的位置,将位置信息更新至sources数组中

拿旧 children 中的节点尝试去新 children 中寻找具有相同 key 值的节点,但并非总是能够找得到,当 k === 'undefined' 时,说明该节点在新 children 中已经不存在了,这时我们应该将其移除

最长递增子序列

什么是最长递增子序列?

给定一个数值序列,找到它的一个子序列,并且子序列中的值是递增的,子序列中的元素在原序列中不一定连续。

例如给定数值序列为:[ 0, 8, 4, 12 ]

那么它的最长递增子序列就是:[0, 8, 12]

当然答案可能有多种情况,例如:[0, 4, 12] 也是可以的

 根据sources计算最长递增子序列LIS

source 数组的值为 [2, 3, 1, -1],很显然最长递增子序列应该是 [ 2, 3 ],但为什么计算出的结果是 [ 0, 1 ] 呢?其实 [ 0, 1 ] 代表的是最长递增子序列中的各个元素在 source 数组中的位置索引 

最长递增子序列的作用?

最长递增子序列是 [ 0, 1 ] 这告诉我们:children 的剩余未处理节点中,位于位置 0 和位置 1 的节点的先后关系与他们在旧 children 中的先后关系相同。或者我们可以理解为位于位置 0 和位置 1 的节点是不需要被移动的节点,即上图中 li-c 节点和 li-d 节点将在接下来的操作中不会被移动。 

 节点新增:索引-1

li-g 节点位置对应的 source 数组元素的值为 -1,这说明 li-g 节点应该作为全新的节点被挂载

节点移动

新节点中的节点不在最长递增子序列且索引不等于-1,并且索引与原老节点索引不同,需要移动老节点。将老节点dom挂载到li-g前面

 vue3源码分析

核心diff算法在core-main\packages\runtime-core\src\renderer.ts文件中

 vue3新增patchFlag

Vue3中的patchFlag是编译器生成的优化提示,用于在执行差异比较时进入“优化模式”。在这种模式下,算法知道虚拟DOM是由编译器生成的渲染函数产生的,因此算法只需要处理这些由补丁标志显式标记的更新。

PatchFlags可以通过位运算符|进行组合,并可以使用&运算符进行检查

PatchFlags枚举了不同类型的补丁标志

 通过组合不同的补丁标志,可以在差异比较过程中针对特定类型的更新进行优化处理。有下面几种类型:

特殊标志:

  • TEXT:表示具有动态textContent(子节点快速路径)的元素。比如{{msg}}
  • CLASS:表示具有动态类绑定的元素。比如“:class='colorStyle'”
  • STYLE:表示具有动态样式的元素。编译器会将静态字符串样式预编译为静态对象,并检测并提升内联静态对象。例如,style="color: red":style="{ color: 'red' }"都会被提升为静态对象{ color: 'red' },以便在渲染函数中使用。

  • PROPS:表示具有非类/样式动态属性的元素,或者是具有任何动态属性(包括类/样式)的组件。当存在这个标志时,虚拟节点还会有一个dynamicProps数组,其中包含可能发生变化的属性键,以便运行时可以更快地进行差异比较(无需担心已删除的属性)。

  • FULL_PROPS:表示具有具有动态键的属性的元素。当键发生变化时,总是需要进行完整的差异比较以删除旧键。这个标志与CLASSSTYLEPROPS是互斥的。

  • NEED_HYDRATION:表示需要对属性进行“hydration”(即初始化)的元素,但不一定需要进行补丁操作。例如,事件监听器和带有属性修饰符的v-bind

  • STABLE_FRAGMENT:表示子节点顺序不会改变的片段。

  • KEYED_FRAGMENT:表示具有带有key或部分带有key的子节点的片段。

  • UNKEYED_FRAGMENT:表示具有无key子节点的片段。

  • NEED_PATCH:表示只需要进行非属性补丁操作的元素,例如ref或指令(onVnodeXXX钩子)。由于每个已补丁的虚拟节点都会检查refonVnodeXXX钩子,因此它只是标记虚拟节点,以便父块可以跟踪它。

  • DYNAMIC_SLOTS:表示具有动态插槽的组件(例如,引用v-for迭代值的插槽,或动态插槽名称)。具有此标志的组件始终会被强制更新。

  • DEV_ROOT_FRAGMENT:表示仅因用户在模板的根级别放置了注释而创建的片段。这是一个仅用于开发环境的标志,因为在生产环境中会剥离注释。

  • HOISTED:表示一个被提升的静态虚拟节点。这是一个提示,用于在“hydration”过程中跳过整个子树,因为静态内容永远不需要更新。

  • BAIL:表示差异比较算法应该退出优化模式的特殊标志。例如,在由renderSlot()创建的块片段中遇到非编译器生成的插槽(即手动编写的渲染函数,应始终完全进行差异比较)时,或者手动克隆VNodes时。

patchChildren方法

 入参解析:n1 与 n2 是待比较的两个节点,n1 为旧节点,n2 为新节点。container 是新节点的容器,而 anchor 是一个锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物。optimized 参数是是否开启优化模式的标识。

获取旧子节点和新子节点:首先从传入的参数n1n2中获取旧子节点c1和新子节点c2,同时获取旧节点的形状标志prevShapeFlag

获取补丁标志和形状标志:接着从新节点n2中获取补丁标志patchFlag和形状标志shapeFlag,用于判断子节点的类型和特性。

  1. 根据 patchFlag 进行判断:

    • 如果 patchFlag 是存在 key 值的 Fragment:KEYED_FRAGMENT,则调用 patchKeyedChildren 来继续处理子节点。
    • 如果 patchFlag 是没有设置 key 值的 Fragment: UNKEYED_FRAGMENT,则调用 patchUnkeyedChildren 处理没有 key 值的子节点。
  2. 根据 shapeFlag (元素类型标记)进行判断:

    • 如果新子节点是文本类型,而旧子节点是数组类型,则直接卸载旧节点的子节点。

      • 如果新旧节点类型一致,则直接更新新子节点的文本。
    • 如果旧子节点类型是数组类型

      • 如果新子节点也是数组类型,则调用 patchKeyedChildren 进行完整的 diff。
      • 如果新子节点不是数组类型,则说明不存在新子节点,直接从树中卸载旧节点即可。
    • 如果旧子节点是文本类型,由于已经在一开始就判断过新子节点是否为文本类型,那么此时可以肯定新子节点肯定不为文本类型,则可以直接将元素的文本置为空字符串。
    • 如果新子节点是类型为数组类型,而旧子节点不为数组,说明此时需要在树中挂载新子节点,进行 mount 操作即可。

 patchKeyedChildren方法

定义三个指针,i,e1,e2。分别表示相同前缀指针,旧节点列表尾指针,新节点列表尾指针。在节点移动过程中i和e1,i和e2之间的关系决定循环是否结束,以及是否旧节点多余还是新节点多余。

前缀比较 

首先,代码通过一个while循环对子节点列表的前缀部分进行比较。在每次循环中,会获取当前位置i处的两个节点n1n2,然后判断它们是否是相同类型的节点(通过isSameVNodeType函数)。如果是相同类型的节点,则调用patch函数对这两个节点进行更新操作;如果不是相同类型的节点,则跳出循环。

后缀比较

接着,代码通过另一个while循环对子节点列表的后缀部分进行比较。在每次循环中,会获取当前位置e1e2处的两个节点n1n2,同样判断它们是否是相同类型的节点。如果是相同类型的节点,则同样调用patch函数对这两个节点进行更新操作;如果不是相同类型的节点,则跳出循环。

普通序列+新节点多余

在下面段代码中,首先通过条件判断if (i > e1),如果i已经超过了旧子节点列表的结束索引e1,说明旧节点遍历结束。如果i<=e2,说明新节点有多余节点。需要处理新增的节点。

  1. 挂载新增节点:在处理新增节点的情况下,代码通过一个while循环,将新增的节点依次挂载到父容器中。具体操作是调用patch函数,将新增节点添加到父容器中,并根据情况设置适当的锚点位置。这样可以确保新增的节点能够正确地插入到子节点列表中。

  2. 克隆节点处理:在处理新增节点时,代码会根据是否优化的标志optimized来决定是否需要克隆节点。如果需要优化,则会调用cloneIfMounted函数对节点进行克隆处理;否则会调用normalizeVNode函数对节点进行规范化处理。

普通序列+旧节点多余

在下面段代码中,通过条件判断else if (i > e2),如果i已经超过了新子节点列表的结束索引e12,说明新节点遍历结束。如果i<=e1,说明旧节点有多余节点。需要删除旧节点多余的元素。

  1. 卸载节点:在处理需要卸载的节点的情况下,代码通过一个while循环,将需要卸载的节点依次进行卸载操作。具体操作是调用unmount函数,将节点从父容器中卸载,并根据情况传入相应的参数,如parentComponentparentSuspense等。

  2. 卸载操作细节:在卸载节点时,代码会传入参数true,表示需要强制卸载节点。这样可以确保节点在卸载时能够正确地执行清理操作,如解绑事件监听器、清除定时器等。

 未知序列

将前缀节点i,扩展为旧节点头指针s1,新节点头指针s2。

构建新节点的索引映射表
  1. 构建新子节点的key:index映射:在处理未知序列的情况下,首先定义了两个起始索引s1s2,分别表示前一个子节点列表和后一个子节点列表的起始索引。

  2. 构建key:index映射:接着通过循环遍历后一个子节点列表,对每个子节点进行处理。对于每个子节点,会先进行克隆或规范化处理,然后判断是否存在key属性。如果存在key属性,则将key与当前索引i建立映射关系,并存储在keyToNewIndexMap中。

  3. 重复key检查:在存储key与索引映射关系时,代码会进行重复key检查,确保每个key在映射中是唯一的。如果发现重复的key,则会在开发环境下给出警告提示,提示开发者需要确保key的唯一性。

构建newIndexToOldIndexMap:通过循环初始化newIndexToOldIndexMap数组,用于记录新旧子节点之间的索引映射关系。其中,newIndexToOldIndexMap的索引表示新子节点的索引,值表示对应的旧子节点的索引(偏移了1,0表示新节点没有对应的旧节点)。

遍历旧节点列表+填充newIndexToOldIndexMap

遍历旧子节点列表:接着通过循环遍历旧子节点列表,对每个旧子节点进行处理。如果已经处理的节点数量patched超过了需要处理的节点数量toBePatched,则表示所有新子节点已经处理完毕,剩下的旧子节点需要被移除。

匹配新旧节点:对于每个旧子节点,首先尝试通过key值在keyToNewIndexMap中查找对应的新子节点索引。如果未找到对应的key,则尝试在新子节点列表中查找类型相同的无key节点进行匹配。

更新节点:如果成功找到对应的新子节点索引newIndex,则更新newIndexToOldIndexMap中的映射关系,并调用patch函数对旧子节点和新子节点进行更新操作,包括属性更新、DOM操作等。

 移除节点:如果未找到对应的新子节点索引,说明该旧子节点在新子节点列表中已经不存在,需要将其移除,调用unmount函数进行卸载操作。

 最长递增子序列+移动挂载逻辑
  1. 生成最长稳定子序列:在代码中首先判断是否有节点发生移动(moved为真),如果有节点移动,则调用getSequence函数生成最长稳定子序列increasingNewIndexSequence,用于确定哪些节点需要移动。

  2. 逆序遍历处理节点:接着通过逆序遍历处理需要更新的节点。从最后一个节点开始向前遍历,以便使用最后一个已更新的节点作为锚点。

  3. 挂载新节点:对于未在newIndexToOldIndexMap中找到对应关系的节点(值为0),表示这是新节点,需要挂载到DOM树上。调用patch函数进行挂载操作。

  4. 移动节点:如果存在节点移动(moved为真),则需要判断是否需要移动节点。判断条件为:没有稳定子序列(例如逆序情况)或当前节点不在稳定子序列中。根据判断结果,调用move函数进行节点移动操作。

 patchUnkeydChildren方法

通过遍历子节点列表,对公共部分进行更新,移除多余的旧节点,挂载剩余的新节点,可以实现对没有key的子节点列表的更新和维护。

 

 

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

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

相关文章

C++ 之 【类与对象】从入门到精通一条龙服务 最终篇(static成员、友元、匿名对象。。。)

&#x1f4b4;到用时方恨早&#xff0c;白首方悔挣的少 车到山前没有路&#xff0c;悬崖勒马勒不住 一、再谈构造函数 1.构造函数体赋值 2.初始化列表 3.explicit关键字 二、Static成员 1.概念 2.特性 三、友元 1.友元函数 2.友元类 四、内部类 五、匿名对象 六、…

【uniapp】vscode安装插件、ts校验、允许json文件注释

1、vscode安装的插件&#xff1a; uni-create-viewuni-hlperuniapp小程序扩展 2、ts校验 安装插件&#xff1a; pnpm i -D types/wechat-miniprogram uni-helper/uni-app-types配置tsconfig.json {"extends": "vue/tsconfig/tsconfig.json","compi…

向量 | vector;标量 | scalar;矩阵;张量

目录 什么是标量 什么是向量? 向量的3种表达方式 向量的矩阵表示 什么是矩阵 什么是张量 什么是标量 标量只有大小概念,没有方向的概念。通过一个具体的数值就能表达完整。 比如:重量、温度、长度、提及、时间、热量等都数据标量。

gzip,bzip2,xz,tar-读书笔记(九)

gzip 将文件进行压缩 在Linux系统中&#xff0c;gzip 是一个压缩和解压文件的命令工具。它使用LZ77压缩算法及霍夫曼编码&#xff08;Huffman Coding&#xff09;来压缩文件&#xff0c;通常用来减少文件的大小&#xff0c;以节约磁盘空间或减少网络传输的时间。 gzip 命令的…

SpringBoot集成JWT快速入门Demo

目录 1. 概述 2. JWT的请求流程 3. Session认证与JWT认证的区别 4 JWT优缺点 4.1 优点 4.2 缺点 5. 快速入门 5.1 创建工程 5.2 导入依赖 5.3 添加配置文件 5.4 添加Swagger2配置类 5.5 添加JWT工具类 5.6 添加entity、service、controller类 5.7 添加拦截器类 …

【Linux】CentOS 7安装后没有图形界面

专栏文章索引&#xff1a;Linux 有问题可私聊&#xff1a;QQ&#xff1a;3375119339 目录 一、项目场景 二、问题描述 三、原因分析 四、解决方案 1.当前处于命令行界面&#xff0c;可以切换为图形界面 2.安装时没有安装图形界面&#xff0c;选择了Minimal Install 3.下…

【Linux】磁盘分区扩容到原有目录(LVM)

一、LVM基本组成 LVM(Logical Volume Manager&#xff0c;逻辑卷管理)逻辑卷管理器是负责管理物理卷、卷组和逻辑卷的软件层。它提供了创建、调整和管理这些组件的命令和工具&#xff0c;使得用户能够灵活地调整和管理磁盘空间。 1.物理卷&#xff08;PV&#xff0c;Physical…

基于Springcloud可视化项目:智慧工地可视化大数据云平台源码

目录 技术架构 智慧工地系统在实际推行过程中遇到的问题 智慧工地接纳程度较低 基础设施条件有待完善 智慧工地整体生态尚未完善 智慧工地平台各功能模块 施工过程工信程息信管息理管模理块 人员管理模块 生产管理模块 技术管理模块 质量管理模块 安全管理模块 绿…

免费的 ChatGPT 网站(六个)

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 一、insCode二、讯飞星火三、豆包四、文心一言五、通义千问六、360智脑 现在智能…

关于GDAL计算图像坐标的几个问题

关于GDAL计算图像坐标的几个问题_gdal读取菱形四角点坐标-CSDN博客 这篇文章写的很好&#xff0c;讲清楚了图像行列号与图像点坐标&#xff08;x,y&#xff09;对应关系&#xff0c;以及图像行列号如何转为地理坐标的&#xff0c;转载一下做个备份。 1.关于GDAL计算图像坐标的…

【Python】使用OPC UA创建数据服务器

目录 准备工作服务器设置创建或获取节点设置节点值启动服务器查看服务器客户端总结 在工业自动化和物联网&#xff08;IoT&#xff09;领域&#xff0c;OPC UA&#xff08;开放平台通信统一架构&#xff09;已经成为一种广泛采用的数据交换标准。它提供了一种安全、可靠且独立于…

51单片机-独立按键模块

1. 独立按键控制LED状态 轻触按键实现原理&#xff1a;按下时&#xff0c;接通&#xff0c;通过金属弹片受力弹动来实现接通和断开。 松开按键 按下之后&#xff1a;就会被连接 同时按下K1和K2时&#xff0c;P2_0,接口所连LED灯才亮。 #include <REGX52.H> void ma…

【opencv】示例-travelsalesman.cpp 使用模拟退火算法求解旅行商问题

// 载入 OpenCV 的核心头文件 #include <opencv2/core.hpp> // 载入 OpenCV 的图像处理头文件 #include <opencv2/imgproc.hpp> // 载入 OpenCV 的高层GUI(图形用户界面)头文件 #include <opencv2/highgui.hpp> // 载入 OpenCV 的机器学习模块头文件 #includ…

Devin AI: The World’s First AI Software Engineer

Devin AI是Cognition AI团队推出的一款名为Devin的人工智能软件工程师&#xff0c;它被誉为世界上第一个完全自主的AI软件工程师。Devin AI在2024年3月12日发布&#xff0c;并在SWE-bench编码基准测试中设立了新的技术标杆。 Devin AI具备多项强大的能力&#xff0c;包括学习如…

简述OSI七层模型及每层的功能任务和协议

文章目录 一、OSI七层模型的功能和任务1.物理层2.数据链路层3.网络层4.传输层5.会话层6.表示层7. 应用层 二、OSI七层模型每层的协议 开放系统互连参考模型&#xff08;Open System Interconnect&#xff0c;简称OSI&#xff09;是国际标准化组织(ISO)和国际电报电话咨询委员会…

openstack安装dashboard后登录网页显示404错误

1. 2.进入该目录vim /etc/httpd/conf.d/openstack-dashboard.conf 增加这一行 WSGIApplicationGroup %{GLOBAL} 重启httpd后就可以访问了

SpringBoot+FreeMaker

目录 1.FreeMarker说明2.SpringBootFreeMarker快速搭建Pom文件application.properties文件Controller文件目录结构 3.FreeMarker数据类型3.1.布尔类型3.2.数值类型3.3.字符串类型3.4.日期类型3.5.空值类型3.6.sequence类型3.7.hash类型 4.FreeMarker指令assign自定义变量指令if…

[大模型]DeepSeek-7B-chat FastApi 部署调用

DeepSeek-7B-chat FastApi 部署调用 DeepSpeek 介绍 由70亿个参数组成的高级语言模型 DeepSeek LLM。它是在一个包含2万亿个英文和中文代币的庞大数据集上从零开始训练的。为了促进研究&#xff0c;DeepSeek 已经为研究社区开放了DeepSeek LLM 7B/67B Base 和 DeepSeek LLM 7…

软考129-上午题-【软件工程】-McCabe度量法+白盒测试真题

一、真题 真题1&#xff1a; 简单路径&#xff1a; 简单路径是指在一个图中&#xff0c;从一个顶点出发&#xff0c;经过一系列不同的顶点&#xff0c;最终到达另一个顶点&#xff0c;且在整个过程中&#xff0c;除了起点和终点外&#xff0c;每个顶点只被访问一次的路径。在简…

Linux 目录结构与基础查看命令

介绍 目录结构如下 /bin&#xff1a;存放着用户最经常使用的二进制可执行命令&#xff0c;如cp、ls、cat等。这些命令是系统管理员和普通用户进行日常操作所必需的。 /boot&#xff1a;存放启动系统使用的一些核心文件&#xff0c;如引导加载器&#xff08;bootstrap loader…