重庆微信营销网站,编程猫官网,wordpress下单邮件通知,浏览器正能量网站2021虚拟dom——virtual dom#xff0c;提供一种简单js对象去代替复杂的 dom 对象#xff0c;从而优化 dom 操作。virtual dom 是“解决过多的操作 dom 影响性能”的一种解决方案。virtual dom 很多时候都不是最优的操作#xff0c;但它具有普适性#xff0c;在效率、可维护性之… 虚拟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-d、li-a、li-b、li-c如下图所示 头尾比较 如果前面三种都找不到相同节点看头尾能否找到。在下面的例子中旧节点的头和新节点的尾相等。此时移动旧节点的A到旧节点尾部与新节点的相对位置保持一致。之后头指针向后尾指针向前--。 双端非理想乱序——key映射表查找老节点
在之前的讲解中我们所采用的是较理想的例子换句话说在每一轮的比对过程中总会满足四个步骤中的一步但实际上大多数情况下并不会这么理想如下图所示 可以根据节点的key建立一个旧节点key的映射表。然后用新节点头指针newStartVnode遍历新节点根据新节点从映射表里找旧节点如果找到了就复用找不到就创建。复用老节点同时移动老节点这个时候节点在列表中间只需要 将找到的元素比如上图将b移动到a前面同时原有的b要设为undefined因为要保留占位。处理完新节点头指针 考虑过程中出现节点会变成为undefined所以找旧节点时要加判断如果是undefined就跳过 新老节点列表长度不相等 如果新老节点有一方长度不相等循环结束后肯定有一方多余。这个时候要判断是新节点多了还是老节点多了。多余的节点在startIndex和endIndex中间所以判断哪个的startIndexoldIndex。如果老节点多了就删除如果新节点多了就插入。 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)oldStartIndexnewStartIndexoldStartNode 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)oldStartIndexnewStartIndex--oldStartNode prevChildren[oldStartIndex]newEndNode nextChildren[newStartIndex]} else if (oldEndNode.key newStartNode.key) {patch(oldEndNode, newStartNode, parent)parent.insertBefore(oldEndNode.el, oldStartNode.el)oldStartIndex--newStartIndexoldEndNode 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}newStartIndexnewStartNode nextChildren[newStartIndex]}}if (newStartIndex newStartIndex) {while (oldStartIndex oldStartIndex) {if (!prevChildren[oldStartIndex]) {oldStartIndexcontinue}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函数前两个参数位为 oldVnode 和 Vnode 分别代表新的节点和之前的旧节点主要做了四个判断 没有新节点直接触发旧节点的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如果都不满足以上四种情形那说明没有相同的节点可以复用则会分为以下两种情况 从旧的 VNode 为 key 值对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点再进行patchVnode同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置 vue3 diff算法做了哪些优化 静态树提升Static Tree HoistingVue3使用静态树提升技术将静态内容从动态内容中分离出来并在渲染时只更新动态内容从而减少不必要的更新操作提高性能。 PatchFlag引入了PatchFlag标志用于标识虚拟节点的类型和需要进行的操作从而在更新过程中更快地识别和处理特定类型的更新。 Fragments优化针对不同类型的片段如稳定片段、带键片段、无键片段等Vue3采取不同的优化策略以减少不必要的比较和更新操作。 动态插槽优化针对具有动态插槽的组件Vue3会进行特殊处理并始终强制更新这些组件以确保插槽内容正确渲染。 优化的算法逻辑Vue3对diff算法的逻辑进行了优化进行前缀后缀处理并引入最长递增子序列减少元素移动次数。使得在更新过程中能够更快地定位变化并进行更新减少不必要的操作。
什么是PatchFlag vue3会根据元素是否有动态文本动态样式动态类等给不同的元素打上patchFlag标识。在diff算法比较的时候会针对有patchFlag标识的节点进行比对。在Vue3中patchFlag的值是一个整数通过位运算来表示不同的更新操作类型。通过检查patchFlag的值Vue3可以快速了解虚拟节点需要进行的具体操作从而优化更新过程。 divspanhello vue/spanspan{{msg}}/spanspan :classnamepoetry/spanspan :idnamepoetry/spanspan :idname{{msg}}/spanspan :idname :msgmsgpoetry/span
/div 可以看到vue3的模板编译在动态的节点、样式、属性等节点都加上patchFlag编译后的值。对非动态的节点不加patchFlag。
vue2的模板编译就比较简单了 patchFlag的类型 通过组合不同的补丁标志可以在差异比较过程中针对特定类型的更新进行优化处理。有下面几种类型 特殊标志
TEXT1表示具有动态textContent子节点快速路径的元素。比如{{msg}}CLASS11表示具有动态类绑定的元素。比如“:classcolorStyle” STYLE12表示具有动态样式的元素。编译器会将静态字符串样式预编译为静态对象并检测并提升内联静态对象。例如stylecolor: red和:style{ color: red }都会被提升为静态对象{ color: red }以便在渲染函数中使用。 PROPS13表示具有非类/样式动态属性的元素或者是具有任何动态属性包括类/样式的组件。当存在这个标志时虚拟节点还会有一个dynamicProps数组其中包含可能发生变化的属性键以便运行时可以更快地进行差异比较无需担心已删除的属性。 FULL_PROPS14表示具有具有动态键的属性的元素。当键发生变化时总是需要进行完整的差异比较以删除旧键。这个标志与CLASS、STYLE和PROPS是互斥的。 NEED_HYDRATION125表示需要对属性进行“hydration”即初始化的元素但不一定需要进行补丁操作。例如事件监听器和带有属性修饰符的v-bind。 STABLE_FRAGMENT16表示子节点顺序不会改变的片段。 KEYED_FRAGMENT17表示具有带有key或部分带有key的子节点的片段。 UNKEYED_FRAGMENT18表示具有无key子节点的片段。 NEED_PATCH19表示只需要进行非属性补丁操作的元素例如ref或指令onVnodeXXX钩子。由于每个已补丁的虚拟节点都会检查ref和onVnodeXXX钩子因此它只是标记虚拟节点以便父块可以跟踪它。 DYNAMIC_SLOTS110表示具有动态插槽的组件例如引用v-for迭代值的插槽或动态插槽名称。具有此标志的组件始终会被强制更新。 DEV_ROOT_FRAGMENT111表示仅因用户在模板的根级别放置了注释而创建的片段。这是一个仅用于开发环境的标志因为在生产环境中会剥离注释。 HOISTED-1表示一个被提升的静态虚拟节点。这是一个提示用于在“hydration”过程中跳过整个子树因为静态内容永远不需要更新。 BAIL-2表示差异比较算法应该退出优化模式的特殊标志。例如在由renderSlot()创建的块片段中遇到非编译器生成的插槽即手动编写的渲染函数应始终完全进行差异比较时或者手动克隆VNodes时。
什么是HoistStatic? HoistStatic是Vue3中的一个特殊的patchFlag值为-1。用于表示一个被提升的静态虚拟节点。当一个虚拟节点被标记为HoistStatic时这意味着该节点是静态的不需要进行更新操作因为静态内容在渲染过程中永远不会改变。 通过将静态内容标记为HoistStaticVue3可以在“hydration”即将虚拟DOM转换为真实DOM过程中跳过整个子树的更新从而节省时间和资源。这样可以提高渲染性能特别是在处理大型组件树时。 给定下面的代码只有{{msg}}是动态的会导致页面刷新看下vue3的编译函数
divspanhello vue3/spanspanhello vue3/spanspanhello vue3/spanspan{{msg}}/span
/div 可以看到静态节点的定义被提升到父作用域缓存起来。之后函数怎么执行这些变量都不会重新定义一遍。 如果有多个静态节点呢 当静态节点达到一定阈值后会被vue3合并起来 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数组 需要构造一个数组 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表示具有动态类绑定的元素。比如“:classcolorStyle” STYLE表示具有动态样式的元素。编译器会将静态字符串样式预编译为静态对象并检测并提升内联静态对象。例如stylecolor: 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时。 patchChildren方法 入参解析n1 与 n2 是待比较的两个节点n1 为旧节点n2 为新节点。container 是新节点的容器而 anchor 是一个锚点用来标识当我们对新旧节点做增删或移动等操作时以哪个节点为参照物。optimized 参数是是否开启优化模式的标识。 获取旧子节点和新子节点首先从传入的参数n1和n2中获取旧子节点c1和新子节点c2同时获取旧节点的形状标志prevShapeFlag。
获取补丁标志和形状标志接着从新节点n2中获取补丁标志patchFlag和形状标志shapeFlag用于判断子节点的类型和特性。 根据 patchFlag 进行判断 如果 patchFlag 是存在 key 值的 FragmentKEYED_FRAGMENT则调用 patchKeyedChildren 来继续处理子节点。如果 patchFlag 是没有设置 key 值的 Fragment: UNKEYED_FRAGMENT则调用 patchUnkeyedChildren 处理没有 key 值的子节点。 根据 shapeFlag 元素类型标记进行判断 如果新子节点是文本类型而旧子节点是数组类型则直接卸载旧节点的子节点。 如果新旧节点类型一致则直接更新新子节点的文本。 如果旧子节点类型是数组类型 如果新子节点也是数组类型则调用 patchKeyedChildren 进行完整的 diff。如果新子节点不是数组类型则说明不存在新子节点直接从树中卸载旧节点即可。如果旧子节点是文本类型由于已经在一开始就判断过新子节点是否为文本类型那么此时可以肯定新子节点肯定不为文本类型则可以直接将元素的文本置为空字符串。如果新子节点是类型为数组类型而旧子节点不为数组说明此时需要在树中挂载新子节点进行 mount 操作即可。 patchKeyedChildren方法 定义三个指针ie1e2。分别表示相同前缀指针旧节点列表尾指针新节点列表尾指针。在节点移动过程中i和e1i和e2之间的关系决定循环是否结束以及是否旧节点多余还是新节点多余。 前缀比较
首先代码通过一个while循环对子节点列表的前缀部分进行比较。在每次循环中会获取当前位置i处的两个节点n1和n2然后判断它们是否是相同类型的节点通过isSameVNodeType函数。如果是相同类型的节点则调用patch函数对这两个节点进行更新操作如果不是相同类型的节点则跳出循环。 后缀比较
接着代码通过另一个while循环对子节点列表的后缀部分进行比较。在每次循环中会获取当前位置e1和e2处的两个节点n1和n2同样判断它们是否是相同类型的节点。如果是相同类型的节点则同样调用patch函数对这两个节点进行更新操作如果不是相同类型的节点则跳出循环。 普通序列新节点多余
在下面段代码中首先通过条件判断if (i e1)如果i已经超过了旧子节点列表的结束索引e1说明旧节点遍历结束。如果ie2说明新节点有多余节点。需要处理新增的节点。 挂载新增节点在处理新增节点的情况下代码通过一个while循环将新增的节点依次挂载到父容器中。具体操作是调用patch函数将新增节点添加到父容器中并根据情况设置适当的锚点位置。这样可以确保新增的节点能够正确地插入到子节点列表中。 克隆节点处理在处理新增节点时代码会根据是否优化的标志optimized来决定是否需要克隆节点。如果需要优化则会调用cloneIfMounted函数对节点进行克隆处理否则会调用normalizeVNode函数对节点进行规范化处理。 普通序列旧节点多余
在下面段代码中通过条件判断else if (i e2)如果i已经超过了新子节点列表的结束索引e12说明新节点遍历结束。如果ie1说明旧节点有多余节点。需要删除旧节点多余的元素。 卸载节点在处理需要卸载的节点的情况下代码通过一个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的索引表示新子节点的索引值表示对应的旧子节点的索引偏移了10表示新节点没有对应的旧节点。
遍历旧节点列表填充newIndexToOldIndexMap
遍历旧子节点列表接着通过循环遍历旧子节点列表对每个旧子节点进行处理。如果已经处理的节点数量patched超过了需要处理的节点数量toBePatched则表示所有新子节点已经处理完毕剩下的旧子节点需要被移除。
匹配新旧节点对于每个旧子节点首先尝试通过key值在keyToNewIndexMap中查找对应的新子节点索引。如果未找到对应的key则尝试在新子节点列表中查找类型相同的无key节点进行匹配。
更新节点如果成功找到对应的新子节点索引newIndex则更新newIndexToOldIndexMap中的映射关系并调用patch函数对旧子节点和新子节点进行更新操作包括属性更新、DOM操作等。 移除节点如果未找到对应的新子节点索引说明该旧子节点在新子节点列表中已经不存在需要将其移除调用unmount函数进行卸载操作。 最长递增子序列移动挂载逻辑 生成最长稳定子序列在代码中首先判断是否有节点发生移动moved为真如果有节点移动则调用getSequence函数生成最长稳定子序列increasingNewIndexSequence用于确定哪些节点需要移动。 逆序遍历处理节点接着通过逆序遍历处理需要更新的节点。从最后一个节点开始向前遍历以便使用最后一个已更新的节点作为锚点。 挂载新节点对于未在newIndexToOldIndexMap中找到对应关系的节点值为0表示这是新节点需要挂载到DOM树上。调用patch函数进行挂载操作。 移动节点如果存在节点移动moved为真则需要判断是否需要移动节点。判断条件为没有稳定子序列例如逆序情况或当前节点不在稳定子序列中。根据判断结果调用move函数进行节点移动操作。 patchUnkeydChildren方法 通过遍历子节点列表对公共部分进行更新移除多余的旧节点挂载剩余的新节点可以实现对没有key的子节点列表的更新和维护。