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

 

 

  • 30
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三月的一天

你的鼓励将是我前进的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值