赞
踩
虚拟dom——virtual dom,提供一种简单js对象去代替复杂的 dom 对象,从而优化 dom 操作。virtual dom 是“解决过多的操作 dom 影响性能”的一种解决方案。virtual dom 很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达到平衡。
diff 算法是一种优化手段,将前后两个模块进行差异化比较,修补(更新)差异的过程叫做 patch,也叫打补丁。只有当新旧子节点的类型都是多个子节点时,核心
Diff
算法才派得上用场。diff的目的是时间换空间:尽可能通过移动旧节点,复用旧节点DOM元素,减少新增DOM操作。通过首首、尾尾、首尾、尾首以及在旧节点列表遍历等方式逐个试探去找可复用的旧节点。vue3引入了最长递增子序列优化diff:去掉相同的前缀和后缀,也就是首首、尾尾都比较完后剩余的旧节点列表和新节点列表进行diff。在新节点列表中用一个数组,统计新节点出现在旧节点相同元素的index;对这个数组求最长递增子序列。递增子序列的节点不需要移动(即使不连续),因为它们在新旧节点序列中的相对位置是一样的。
在页面数据发生变化,进行组件更新的时候,产生了新节点虚拟dom。这个时候需要将新节点的dom渲染为真实的dom。核心diff算法就是在已知旧节点的DOM结构、旧节点vnode和新子节点的vnode情况下,以较低的成本完成子节点的更新为目的,求解生成子节点dom的系列操作。
如果我将新的虚拟dom转为真实dom,这个过程消耗性能。如果能复用老节点的dom节点,是不是可以减少dom操作啊。
那对于新旧两个dom树而言,如何比较呢?
vue采用的是深度递归+同层比较。深度递归能保证每个节点都能遍历到,同层比较能够缩小比较范围。
比较是否是相同节点;相同节点比较属性,并复用老节点;
如果是相同节点,考虑老节点和新节点的儿子节点情况:
所谓
双端比较
就是新列表和旧列表两个列表的头与尾互相对比,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。
在看diff算法是时候一定要有一个目标——我要尽可能在旧节点列表中找到一个可以复用的节点!这样就可以复用老节点的数据,而避免了新增dom。为什么使用这四种方式?考虑了下面新旧节点列表可能排列的情况:元素的追加、删除、向左翻转、向右翻转、逆序排列等方式
diff的整个优化采用了双指针的方式,在新老节点列表的头尾插了两个指针。在比较的过程中如果新老节点有一方头尾指针重合了,意味着节点遍历结束。这个时候需要终止diff算法。在比较过程中,如果头指针新老节点相等,头指针向后移动,头指针++;如果尾指针相等,尾指针向前移动,尾指针--。
看下下面这种场景,尾部插入数据。仅使用头和头比较,就能找到复用的节点,且不需要移动旧节点。如果新节点有多余元素,将新节点中多余部分插入到dom里。如果老节点有多余元素,删除老节点多余节点。
头部插入数据,只需要尾尾进行比较。当某一方头尾指针重合了结束。此时如果新节点元素多,将多余的元素一起新增dom操作。其余复用老节点。如果老节点多,同理,将多余的老节点删除
如果头和头,尾和尾都比较不成功,还是尝试从两端找相同节点。此时是不是可以交叉比较。这里举一个尾头比较的例子:
旧节点尾指针指向的D和新节点头指针指向的D相等。此时D是不是要找相对位置,要与新节点保持一致,那么就移动旧节点D到旧节点的头部。保持列表顺序一致。
li-d
节点所对应的真实 DOM 原本是最后一个子节点,并且更新之后它应该变成第一个子节点。所以我们需要把li-d
所对应的真实 DOM 移动到最前方即可 。
由于 li-d
节点所对应的真实 DOM 元素已经更新完成且被移动,所以现在真实 DOM 的顺序是:li-d
、li-a
、li-b
、li-c
,如下图所示:
如果前面三种都找不到相同节点,看头尾能否找到。在下面的例子中旧节点的头和新节点的尾相等。此时移动旧节点的A到旧节点尾部,与新节点的相对位置保持一致。之后头指针向后++,尾指针向前--。
在之前的讲解中,我们所采用的是较理想的例子,换句话说,在每一轮的比对过程中,总会满足四个步骤中的一步,但实际上大多数情况下并不会这么理想,如下图所示:
可以根据节点的key建立一个旧节点key的映射表。然后用新节点头指针newStartVnode遍历新节点,根据新节点从映射表里找旧节点,如果找到了就复用,找不到就创建。复用老节点,同时移动老节点,这个时候节点在列表中间,只需要 将找到的元素,比如上图将b移动到a前面,同时原有的b要设为undefined,因为要保留占位。处理完,新节点头指针++
考虑过程中出现节点会变成为undefined,所以找旧节点时要加判断,如果是undefined就跳过
如果新老节点有一方长度不相等;循环结束后,肯定有一方多余。这个时候要判断是新节点多了还是老节点多了。多余的节点在startIndex和endIndex中间,所以判断哪个的startIndex>oldIndex。如果老节点多了就删除,如果新节点多了就插入。
newStartIdx
>newEndIdx
保证新节点遍历结束
- 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)
- }
- }
- }
源码地址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
函数前两个参数位为oldVnode
和Vnode
,分别代表新的节点和之前的旧节点,主要做了四个判断:
- 没有新节点,直接触发旧节点的
destory
钩子- 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用
createElm
- 旧节点和新节点自身一样,通过
sameVnode
判断节点是否一样,一样时,直接调用patchVnode
去处理这两个节点- 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点
核心当然还是新旧节点相同时的patchVnode阶段
patchVnode方法,比较两个节点,并且比较两个节点的孩子节点
patchVnode
主要做了几个判断:
dom
的文本内容为新节点的文本内容DOM
,并且添加进父节点DOM
删除子节点不完全一致,则调用updateChildren
新节点和旧节点是相同节点。比较新旧的子节点是否相等。如果新节点的chidren是个列表,同时旧节点的children也是个列表。此时调用updateChidren新旧的children的比较移动逻辑。这部分是核心diff
while
循环主要处理了以下五种情景:
VNode
节点的 start
相同时,直接 patchVnode
,同时新老 VNode
节点的开始索引都加 1VNode
节点的 end
相同时,同样直接 patchVnode
,同时新老 VNode
节点的结束索引都减 1VNode
节点的 start
和新 VNode
节点的 end
相同时,这时候在 patchVnode
后,还需要将当前真实 dom
节点移动到 oldEndVnode
的后面,同时老 VNode
节点开始索引加 1,新 VNode
节点的结束索引减 1VNode
节点的 end
和新 VNode
节点的 start
相同时,这时候在 patchVnode
后,还需要将当前真实 dom
节点移动到 oldStartVnode
的前面,同时老 VNode
节点结束索引减 1,新 VNode
节点的开始索引加 1VNode
为 key
值,对应 index
序列为 value
值的哈希表中找到与 newStartVnode
一致 key
的旧的 VNode
节点,再进行patchVnode
,同时将这个真实 dom
移动到 oldStartVnode
对应的真实 dom
的前面createElm
创建一个新的 dom
节点放到当前 newStartIdx
的位置
静态树提升(Static Tree Hoisting):Vue3使用静态树提升技术,将静态内容从动态内容中分离出来,并在渲染时只更新动态内容,从而减少不必要的更新操作,提高性能。
PatchFlag:引入了PatchFlag
标志,用于标识虚拟节点的类型和需要进行的操作,从而在更新过程中更快地识别和处理特定类型的更新。
Fragments优化:针对不同类型的片段(如稳定片段、带键片段、无键片段等),Vue3采取不同的优化策略,以减少不必要的比较和更新操作。
动态插槽优化:针对具有动态插槽的组件,Vue3会进行特殊处理,并始终强制更新这些组件,以确保插槽内容正确渲染。
优化的算法逻辑:Vue3对diff算法的逻辑进行了优化,进行前缀后缀处理,并引入最长递增子序列,减少元素移动次数。使得在更新过程中能够更快地定位变化并进行更新,减少不必要的操作。
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,表示具有具有动态键的属性的元素。当键发生变化时,总是需要进行完整的差异比较以删除旧键。这个标志与CLASS
、STYLE
和PROPS
是互斥的。
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
钩子)。由于每个已补丁的虚拟节点都会检查ref
和onVnodeXXX
钩子,因此它只是标记虚拟节点,以便父块可以跟踪它。
DYNAMIC_SLOTS
:1<<10,表示具有动态插槽的组件(例如,引用v-for
迭代值的插槽,或动态插槽名称)。具有此标志的组件始终会被强制更新。
DEV_ROOT_FRAGMENT
:1<<11,表示仅因用户在模板的根级别放置了注释而创建的片段。这是一个仅用于开发环境的标志,因为在生产环境中会剥离注释。
HOISTED
:-1,表示一个被提升的静态虚拟节点。这是一个提示,用于在“hydration”过程中跳过整个子树,因为静态内容永远不需要更新。
BAIL
:-2,表示差异比较算法应该退出优化模式的特殊标志。例如,在由renderSlot()
创建的块片段中遇到非编译器生成的插槽(即手动编写的渲染函数,应始终完全进行差异比较)时,或者手动克隆VNodes
时。
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
的diff
借鉴于inferno (opens new window),该算法其中有两个理念。第一个是相同的前置与后置元素的预处理;第二个则是最长递增子序列,
如图所示,新旧
children
拥有相同的前缀节点和后缀节点
对于前缀节点,我们可以建立一个索引j,指向新旧
children
中的第一个节点,并逐步向后遍历,直到遇到两个拥有不同key
值的节点为止
我们需要处理的是相同的后缀节点,由于新旧
children
中节点的数量可能不同,所以我们需要两个索引prevEnd、nextEnd
分别指向新旧children
的最后一个节点,并逐步向前遍历,直到遇到两个拥有不同key
值的节点为止
j > prevEnd
并且j <= nextEnd
此时新节点列表还有节点
新
children
中位于j
到nextEnd
之间的所有节点都应该是新插入的节点
j <= prevEnd
并且j > nextEnd
此时旧节点列表有多余节点
旧
children
中有位于索引j
到prevEnd
之间的节点,都应该被移除
下面这个案例在预处理步骤之后,只有
li-a
节点和li-e
节点能够被提前patch
。换句话说在这种情况下没有办法简单的通过预处理就能够结束Diff
逻辑。这时我们就需要进行下一步操作
需要构造一个数组
source
,该数组的长度等于新children
在经过预处理之后剩余未处理节点的数量,并且该数组中每个元素的初始值为-1。
那么这个数组的作用是什么呢?该数组中的每一个元素分别与新children
中剩余未处理的节点对应,实际上source
数组将用来存储新children
中的节点在旧children
中的位置,后面将会使用它计算出一个最长递增子序列,并用于 DOM 移动。
新增一个映射表存储旧节点的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
节点将在接下来的操作中不会被移动。
与
li-g
节点位置对应的source
数组元素的值为-1
,这说明li-g
节点应该作为全新的节点被挂载
新节点中的节点不在最长递增子序列且索引不等于-1,并且索引与原老节点索引不同,需要移动老节点。将老节点dom挂载到li-g前面
核心diff算法在core-main\packages\runtime-core\src\renderer.ts文件中
Vue3中的
patchFlag
是编译器生成的优化提示,用于在执行差异比较时进入“优化模式”。在这种模式下,算法知道虚拟DOM是由编译器生成的渲染函数产生的,因此算法只需要处理这些由补丁标志显式标记的更新。
PatchFlags
可以通过位运算符|
进行组合,并可以使用&
运算符进行检查。
PatchFlags
枚举了不同类型的补丁标志
通过组合不同的补丁标志,可以在差异比较过程中针对特定类型的更新进行优化处理。有下面几种类型:
特殊标志:
TEXT
:表示具有动态textContent
(子节点快速路径)的元素。比如{{msg}}CLASS
:表示具有动态类绑定的元素。比如“:class='colorStyle'”STYLE
:表示具有动态样式的元素。编译器会将静态字符串样式预编译为静态对象,并检测并提升内联静态对象。例如,style="color: red"
和:style="{ color: 'red' }"
都会被提升为静态对象{ color: 'red' }
,以便在渲染函数中使用。
PROPS
:表示具有非类/样式动态属性的元素,或者是具有任何动态属性(包括类/样式)的组件。当存在这个标志时,虚拟节点还会有一个dynamicProps
数组,其中包含可能发生变化的属性键,以便运行时可以更快地进行差异比较(无需担心已删除的属性)。
FULL_PROPS
:表示具有具有动态键的属性的元素。当键发生变化时,总是需要进行完整的差异比较以删除旧键。这个标志与CLASS
、STYLE
和PROPS
是互斥的。
NEED_HYDRATION
:表示需要对属性进行“hydration”(即初始化)的元素,但不一定需要进行补丁操作。例如,事件监听器和带有属性修饰符的v-bind
。
STABLE_FRAGMENT
:表示子节点顺序不会改变的片段。
KEYED_FRAGMENT
:表示具有带有key
或部分带有key
的子节点的片段。
UNKEYED_FRAGMENT
:表示具有无key
子节点的片段。
NEED_PATCH
:表示只需要进行非属性补丁操作的元素,例如ref
或指令(onVnodeXXX
钩子)。由于每个已补丁的虚拟节点都会检查ref
和onVnodeXXX
钩子,因此它只是标记虚拟节点,以便父块可以跟踪它。
DYNAMIC_SLOTS
:表示具有动态插槽的组件(例如,引用v-for
迭代值的插槽,或动态插槽名称)。具有此标志的组件始终会被强制更新。
DEV_ROOT_FRAGMENT
:表示仅因用户在模板的根级别放置了注释而创建的片段。这是一个仅用于开发环境的标志,因为在生产环境中会剥离注释。
HOISTED
:表示一个被提升的静态虚拟节点。这是一个提示,用于在“hydration”过程中跳过整个子树,因为静态内容永远不需要更新。
BAIL
:表示差异比较算法应该退出优化模式的特殊标志。例如,在由renderSlot()
创建的块片段中遇到非编译器生成的插槽(即手动编写的渲染函数,应始终完全进行差异比较)时,或者手动克隆VNodes
时。
入参解析:n1 与 n2 是待比较的两个节点,n1 为旧节点,n2 为新节点。container 是新节点的容器,而 anchor 是一个锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物。optimized 参数是是否开启优化模式的标识。
获取旧子节点和新子节点:首先从传入的参数n1
和n2
中获取旧子节点c1
和新子节点c2
,同时获取旧节点的形状标志prevShapeFlag
。
获取补丁标志和形状标志:接着从新节点n2
中获取补丁标志patchFlag
和形状标志shapeFlag
,用于判断子节点的类型和特性。
根据 patchFlag 进行判断:
根据 shapeFlag (元素类型标记)进行判断:
如果新子节点是文本类型,而旧子节点是数组类型,则直接卸载旧节点的子节点。
如果旧子节点类型是数组类型
定义三个指针,i,e1,e2。分别表示相同前缀指针,旧节点列表尾指针,新节点列表尾指针。在节点移动过程中i和e1,i和e2之间的关系决定循环是否结束,以及是否旧节点多余还是新节点多余。
首先,代码通过一个while
循环对子节点列表的前缀部分进行比较。在每次循环中,会获取当前位置i
处的两个节点n1
和n2
,然后判断它们是否是相同类型的节点(通过isSameVNodeType
函数)。如果是相同类型的节点,则调用patch
函数对这两个节点进行更新操作;如果不是相同类型的节点,则跳出循环。
接着,代码通过另一个while
循环对子节点列表的后缀部分进行比较。在每次循环中,会获取当前位置e1
和e2
处的两个节点n1
和n2
,同样判断它们是否是相同类型的节点。如果是相同类型的节点,则同样调用patch
函数对这两个节点进行更新操作;如果不是相同类型的节点,则跳出循环。
在下面段代码中,首先通过条件判断if (i > e1)
,如果i
已经超过了旧子节点列表的结束索引e1
,说明旧节点遍历结束。如果i<=e2,说明新节点有多余节点。需要处理新增的节点。
挂载新增节点:在处理新增节点的情况下,代码通过一个while
循环,将新增的节点依次挂载到父容器中。具体操作是调用patch
函数,将新增节点添加到父容器中,并根据情况设置适当的锚点位置。这样可以确保新增的节点能够正确地插入到子节点列表中。
克隆节点处理:在处理新增节点时,代码会根据是否优化的标志optimized
来决定是否需要克隆节点。如果需要优化,则会调用cloneIfMounted
函数对节点进行克隆处理;否则会调用normalizeVNode
函数对节点进行规范化处理。
在下面段代码中,通过条件判断else if (i > e2)
,如果i
已经超过了新子节点列表的结束索引e1
2,说明新节点遍历结束。如果i<=e1,说明旧节点有多余节点。需要删除旧节点多余的元素。
卸载节点:在处理需要卸载的节点的情况下,代码通过一个while
循环,将需要卸载的节点依次进行卸载操作。具体操作是调用unmount
函数,将节点从父容器中卸载,并根据情况传入相应的参数,如parentComponent
和parentSuspense
等。
卸载操作细节:在卸载节点时,代码会传入参数true
,表示需要强制卸载节点。这样可以确保节点在卸载时能够正确地执行清理操作,如解绑事件监听器、清除定时器等。
将前缀节点i,扩展为旧节点头指针s1,新节点头指针s2。
构建新子节点的key:index映射:在处理未知序列的情况下,首先定义了两个起始索引s1
和s2
,分别表示前一个子节点列表和后一个子节点列表的起始索引。
构建key:index映射:接着通过循环遍历后一个子节点列表,对每个子节点进行处理。对于每个子节点,会先进行克隆或规范化处理,然后判断是否存在key
属性。如果存在key
属性,则将key
与当前索引i
建立映射关系,并存储在keyToNewIndexMap
中。
重复key检查:在存储key
与索引映射关系时,代码会进行重复key
检查,确保每个key
在映射中是唯一的。如果发现重复的key
,则会在开发环境下给出警告提示,提示开发者需要确保key
的唯一性。
构建newIndexToOldIndexMap
:通过循环初始化newIndexToOldIndexMap
数组,用于记录新旧子节点之间的索引映射关系。其中,newIndexToOldIndexMap
的索引表示新子节点的索引,值表示对应的旧子节点的索引(偏移了1,0表示新节点没有对应的旧节点)。
遍历旧子节点列表:接着通过循环遍历旧子节点列表,对每个旧子节点进行处理。如果已经处理的节点数量patched
超过了需要处理的节点数量toBePatched
,则表示所有新子节点已经处理完毕,剩下的旧子节点需要被移除。
匹配新旧节点:对于每个旧子节点,首先尝试通过key
值在keyToNewIndexMap
中查找对应的新子节点索引。如果未找到对应的key
,则尝试在新子节点列表中查找类型相同的无key
节点进行匹配。
更新节点:如果成功找到对应的新子节点索引newIndex
,则更新newIndexToOldIndexMap
中的映射关系,并调用patch
函数对旧子节点和新子节点进行更新操作,包括属性更新、DOM操作等。
移除节点:如果未找到对应的新子节点索引,说明该旧子节点在新子节点列表中已经不存在,需要将其移除,调用unmount
函数进行卸载操作。
生成最长稳定子序列:在代码中首先判断是否有节点发生移动(moved
为真),如果有节点移动,则调用getSequence
函数生成最长稳定子序列increasingNewIndexSequence
,用于确定哪些节点需要移动。
逆序遍历处理节点:接着通过逆序遍历处理需要更新的节点。从最后一个节点开始向前遍历,以便使用最后一个已更新的节点作为锚点。
挂载新节点:对于未在newIndexToOldIndexMap
中找到对应关系的节点(值为0),表示这是新节点,需要挂载到DOM树上。调用patch
函数进行挂载操作。
移动节点:如果存在节点移动(moved
为真),则需要判断是否需要移动节点。判断条件为:没有稳定子序列(例如逆序情况)或当前节点不在稳定子序列中。根据判断结果,调用move
函数进行节点移动操作。
通过遍历子节点列表,对公共部分进行更新,移除多余的旧节点,挂载剩余的新节点,可以实现对没有
key
的子节点列表的更新和维护。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。