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

虚拟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的子节点列表的更新和维护。

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

BackTrader 中文文档(十一)

原文&#xff1a;www.backtrader.com/ 基准测试 原文&#xff1a;www.backtrader.com/docu/observer-benchmark/benchmarking/ 票号 #89 是关于添加对资产的基准测试的。理智的做法是&#xff0c;即使有一种策略&#xff0c;即使是正的&#xff0c;也低于简单跟踪资产将提供的内…

第⑭讲:Ceph集群管理:守护进程管理、日志管理和端口号配置

文章目录 1.Ceph各组件守护进程的管理方式2.守护进程管理操作2.1.Ceph所有组件的守护进程列表2.2.重启当前主机中所有的Ceph组件2.3.重启主机中所有的Monitor组件2.4.重启指定主机的Monitor组件2.5.重启指定的OSD组件 3.Ceph的日志管理4.Ceph集群各组件的守护进程5.Ceph集群各组…

All in One:Prometheus 多实例数据统一管理最佳实践

作者&#xff1a;淡唯&#xff08;啃唯&#xff09;、阳其凯&#xff08;逸陵&#xff09; 引言 Prometheus 作为目前最主流的可观测开源项目之一&#xff0c;已经成为云原生监控的事实标准&#xff0c;被众多企业广泛应用。在使用 Prometheus 的时候&#xff0c;我们经常会遇…

自然语言处理: 第二十六章大模型基底之Mistral 8x7B

文章地址: 2401.04088.pdf (arxiv.org) 项目地址: mistralai/mistral-src: Reference implementation of Mistral AI 7B v0.1 model 前言: 本文意在一文深度剖析Mistral 8X7B的关键改进点。 Mistral AI是一个由DeepMind和Meta的三位前员工在巴黎共同创立的AI公司。其在23年…

tsconfig.json文件常用配置

最近在学ts&#xff0c;因为tsconfig的配置实在太多啦&#xff0c;所以写此文章用作记录&#xff0c;也作分享 作用&#xff1f; tsconfig.jsono是ts编译器的配置文件&#xff0c;ts编译器可以根据它的信息来对代码进行编译 初始化一个tsconfig文件 tsc -init配置参数解释 …

股票开户佣金最低多少?万一!A股开户多少钱合适?

开户佣金 通常情况下&#xff0c;股票开户佣金只要在达成交易的前提才收手续的费用&#xff0c;即买入和卖出的时候。目前&#xff0c;国规定收取最高佣金的比例为千分之三。 也就是说&#xff0c;最高为成交金额的3%&#xff0c;一般都会小于这个比例。最低交易佣金是5元起&a…

mac中的VirtualBox不能分配USB设备到虚拟电脑

mac中的VirtualBox不能分配USB设备到虚拟电脑 检查工具-> 扩展包是否安装 Oracle_VM_VirtualBox_Extension_Pack-7.0.14.vbox-extpack检查usb设备是否打开 检查权限 允许VirtualBox访问&#xff1a;在“安全性与隐私”窗口中&#xff0c;选择“隐私”标签。 在左侧的列表中…

微信过期文件怎么恢复?四个高招助你轻松解决(2024新版)

“微信的文件未下载的情况下过期了&#xff0c;平时微信也没有登录在电脑上&#xff0c;之前也没有进行过数据备份&#xff0c;如何找回这个文件啊&#xff01;&#xff01;感谢回答&#xff01;” “急求&#xff0c;当时忘记点击下载&#xff0c;现在急用微信文件下载不了&a…

Go gin框架(详细版)

目录 0. 为什么会有Go 1. 环境搭建 2. 单-请求&&返回-样例 3. RESTful API 3.1 首先什么是RESTful API 3.2 Gin框架支持RESTful API的开发 4. 返回前端代码 go.main index.html 5. 添加静态文件 main.go 改动的地方 index.html 改动的地方 style.css 改动…

【Linux网络编程】TCP协议

TCP协议 1.TCP协议段格式4位首位长度序号和确认序号16位窗口大小6个标志位 2.确认应答机制3.超时重传机制4.连接管理机制如何理解连接如何理解三次握手如何理解四次挥手 5.流量控制6.滑动窗口7.拥塞控制8.延迟应答9.捎带应答10.面向字节流11.粘包问题12.TCP异常情况13.TCP小结1…

通讯录的实现(单链表版本)

我们首先要知道通讯录的实现是基于单链表的基础上的&#xff0c;所以我们首先要搞懂单链表。&#xff08;注意&#xff1a;今天的代码量较多&#xff09;&#xff0c;但这不是阻挡我们前进的脚步&#xff0c;冲冲冲&#xff01;&#xff01;&#xff01; 单链表的简要概述 我们…

剖析 SPI 在 Spring 中的应用

一、概述 SPI&#xff08;Service Provider Interface&#xff09;&#xff0c;是Java内置的一种服务提供发现机制&#xff0c;可以用来提高框架的扩展性&#xff0c;主要用于框架的开发中&#xff0c;比如Dubbo&#xff0c;不同框架中实现略有差异&#xff0c;但核心机制相同…

构建第一个ArkTS应用之stateStyles:多态样式

Styles和Extend仅仅应用于静态页面的样式复用&#xff0c;stateStyles可以依据组件的内部状态的不同&#xff0c;快速设置不同样式。这就是我们本章要介绍的内容stateStyles&#xff08;又称为&#xff1a;多态样式&#xff09;。 概述 stateStyles是属性方法&#xff0c;可以…

如何发布自己的Python库?

Python包发布 1、背景概述2、操作指南 1、背景概述 为什么我们要发布自己的Python库&#xff1f;如果你想让你的Python代码&#xff0c;通过pip install xxx的方式供所有人下载&#xff0c;那就需要将代码上传到PyPi上&#xff0c;这样才能让所有人使用 那么&#xff0c;如何发…

【最新整理】3ds Max 大佬都在用的10款爆火插件推荐!

在3D建模和渲染领域&#xff0c;熟悉使用各种插件已经成为了大佬们的标配&#xff0c;而3ds Max作为最受欢迎的三维建模软件之一&#xff0c;更是有着丰富的插件资源。今天&#xff0c;小编将为大家盘点一下最新整理的10款爆火插件&#xff0c;这些插件不仅能够提升你的工作效率…

集合体系java

Collection:单列集合&#xff1a;每个元素只包含一个值 Collection集合存储的是地址 Collection的三种遍历方法如下 //迭代器是用来遍历集合的专用方式&#xff08;数组没有迭代器&#xff09;&#xff0c;在java中迭代器的代表是Iterator //boolean hasNext():询问当前位置…

10万字208道Java经典面试题总结(2024修订版)- SSM篇

&#x1f345; 作者简介&#xff1a;哪吒&#xff0c;CSDN2021博客之星亚军&#x1f3c6;、新星计划导师✌、博客专家&#x1f4aa; &#x1f345; 哪吒多年工作总结&#xff1a;Java学习路线总结&#xff0c;搬砖工逆袭Java架构师 &#x1f345; 技术交流&#xff1a;定期更新…

(三)C++自制植物大战僵尸游戏项目结构说明

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/ErelL 一、项目结构 打开项目后&#xff0c;在解决方案管理器中有五个项目&#xff0c;分别是libbox2d、libcocos2d、librecast、libSpine、PlantsVsZombies五个项目&#xff0c;除PlantsVsZombies外&#xff0c;其他四个…

map与set

set使用 set在我们就是我们前面学习的k模型&#xff0c;它可以用来比对数据&#xff0c;增删查的时间复杂度都是O&#xff08;logn&#xff09;效率非常高&#xff0c;由于它底层的原因&#xff0c;它也可以实现排序&#xff0c;通过中序遍历可以输出我们的有序的数据&#xff…

#新版Onenet云平台使用(ESP8266 AT指令上报数据以及公网MQTT服务器连接测试)

1.上云方式&#xff1a;MQTT 参考&#xff1a; 新版ONENET物联网开放平台ATMQTT指令连接_at指令连接onenet的mqtt-CSDN博客https://blog.csdn.net/lilbye/article/details/131770196 ESP8266-01s入门&#xff1a;AT指令讲解、上云与MQTT通信教程-物联沃-IOTWORD物联网https:…