虚拟DOM部分内容详见Vue虚拟DOM详解 | QT-7274 (qblog.top)
diff算法
现在如果要明确两个树结构的差异并进行转换,大致会分为一下两个步骤: 第一阶段:先循环遍历每一个节点进行比较,即针对旧树的每一个节点,都与新树的所有节点进行比较; 第二阶段:在遍历比较结束后,为了做出必要的修改,再次遍历整个树进行实际的操作(添加、删除、修改等)。
代码详解
初始化一个空数组
result
,用于存储比较结果。定义
diff
函数,接受两个参数:beforeLeaf
和afterLeaf
,分别表示两个要比较的HTML元素树。获取两个元素树中较大的子节点数量,并将其赋值给变量
count
。使用
for
循环遍历从0到
count-1
的索引。- 如果
beforeLeaf
的子节点不存在,将afterLeaf
的子节点添加到result
数组中,类型为"add"。 - 如果
afterLeaf
的子节点不存在,将beforeLeaf
的子节点从result
数组中移除,类型为"remove"。 - 如果两个子节点的标签名不同,将
beforeLeaf
的子节点移除,将afterLeaf
的子节点添加到result
数组中,类型分别为"remove"和"add"。 - 如果两个子节点的标签名相同但内容不同,进行以下操作:
- 如果子节点没有子节点,将
beforeLeaf
的子节点替换为afterLeaf
的子节点,类型为"changed",并记录更改后的HTML内容。 - 如果子节点有子节点,递归调用
diff
函数进行比较。
- 如果子节点没有子节点,将
- 如果
返回存储了所有差异的
result
数组。// 定义一个空数组,用于存储比较结果 let result = []; // 定义一个名为diff的函数,用于比较两个叶子节点(beforeLeaf和afterLeaf) const diff = function (beforeLeaf, afterLeaf) { // 获取两个节点树中较长的那个的长度 let count = Math.max(beforeLeaf.children.length, afterLeaf.children.length); // 遍历两个节点树的子节点 for (let i = 0; i< count; i++) { // 获取beforeLeaf和afterLeaf的子节点 const beforeTag = beforeLeaf.children[i]; const afterTag = afterLeaf.children[i]; // 如果beforeTag不存在,说明在afterLeaf中添加了一个新的节点 if (beforeTag === undefined) { result.push({ type: "add", element: afterTag }); // 如果afterTag不存在,说明在beforeLeaf中删除了一个节点 } else if (afterTag === undefined) { result.push({ type: "remove", element: beforeTag }); // 如果两个节点的标签名不同,说明需要删除旧的节点并添加新的节点 } else if (beforeTag.tagName !== afterTag.tagName) { result.push({ type: "remove", element: beforeTag }); result.push({ type: "add", element: afterTag }); // 如果两个节点的标签名相同但内容不同 } else if (beforeTag.innerHTML !== afterTag.innerHTML) { // 如果节点没有子节点,直接替换节点的内容 if (beforeTag.children.length === 0) { result.push({ type: "changed", beforeElement: beforeTag, afterElement: afterTag, html: afterTag.innerHTML }); } else { // 如果节点有子节点,递归调用diff函数进行比较 diff(beforeTag, afterTag); } } } // 返回比较结果 return result; }
Vue中的diff算法
当数据发生改变时,Vue的响应式更新会调用Dep.notify()方法来通知所有订阅者,每个订阅者便会调用patch方法修改真实DOM,更新视图。
如果符合SameNode,就不会渲染Vnode重新创建DOM节点,而是在原有的DOM节点上进行修补,尽可能复用原有的DOM节点——以上的思想主要体现在patchNode方法中。
patchNode代码详解
主要目的是比较新旧虚拟节点(oldVnode
和 vnode
),并根据它们之间的差异来更新实际的DOM元素(elm
)。
首先,获取旧虚拟节点的子节点(
oldCh
)和新虚拟节点的子节点(ch
)。接下来,检查新虚拟节点(
vnode
)的文本内容是否为undefined
。如果是,说明它有子节点。如果新旧虚拟节点都有子节点(
oldCh
和ch
都不为undefined
),则比较它们的子节点是否相同。如果不同,调用updateChildren
函数来更新实际DOM元素的子节点。如果只有新虚拟节点有子节点(
ch
不为undefined
,oldCh
为undefined
),首先检查旧虚拟节点是否有文本内容。如果有,清空实际DOM元素的文本内容。然后,使用addVnodes
函数将新虚拟节点的子节点添加到实际DOM元素中。如果只有旧虚拟节点有子节点(
oldCh
不为undefined
,ch
为undefined
),使用removeVnodes
函数从实际DOM元素中移除旧虚拟节点的子节点。如果旧虚拟节点有文本内容,但新虚拟节点没有子节点,清空实际DOM元素的文本内容。
如果新旧虚拟节点都只有文本内容,且它们的文本内容不同,使用
nodeOps.setTextContent
函数更新实际DOM元素的文本内容。// 获取旧的虚拟节点和新的虚拟节点的子节点 const oldCh = oldVnode.children; const ch = vnode.children; // 如果新的虚拟节点没有文本内容 if (isUndef(vnode.text)) { // 如果旧的虚拟节点和新的虚拟节点都有子节点,调用updateChildren进行子节点的更新 if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } // 如果只有新的虚拟节点有子节点 else if (isDef(ch)) { // 如果旧的虚拟节点有文本内容,先清空文本内容 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // 添加新的子节点到实际DOM中 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } // 如果只有旧的虚拟节点有子节点 else if (isDef(oldCh)) { // 移除旧的子节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1) } // 如果旧的虚拟节点有文本内容但没有子节点 else if (isDef(oldVnode.text)) { // 清空实际DOM的文本内容 nodeOps.setTextContent(elm, '') } } // 如果旧的虚拟节点和新的虚拟节点都只有文本内容,且文本内容不同 else if (oldVnode.text !== vnode.text) { // 更新实际DOM的文本内容 nodeOps.setTextContent(elm, vnode.text) }
而对于这四种情况,首先要明确diff算法的整体策略:深度优先,同层比较。
也就是说,从根节点开始,在比较之前,首先判断新旧两个根节点是否有子节点,都有子节点则进入下一层级,以此类推,直到发现都没有子节点时,进入同层比较阶段。当最底层的比较结束后,则返回上一层级继续同层比较。下面首先针对oldVnode和Vnode都有子节点的情况,分析其如何尽可能少地进行DOM节点的操作。
- 旧首节点和新首节点用
sameNode
对比。 - 旧尾节点和新尾节点用
sameNode
对比 - 旧首节点和新尾节点用
sameNode
对比 - 旧尾节点和新首节点用
sameNode
对比 - 如果以上逻辑都匹配不到,再把所有旧子节点的
key
做一个映射到旧节点下标的key -> index
表,然后用新vnode
的key
去找出在旧节点中可以复用的位置。
然后不停的把匹配到的指针向内部收缩,直到新旧节点有一端的指针相遇(说明这个端的节点都被patch过了)。
在指针相遇以后,还有两种比较特殊的情况:
- 有新节点需要加入。 如果更新完以后,
oldStartIdx > oldEndIdx
,说明旧节点都被patch
完了,但是有可能还有新的节点没有被处理到。接着会去判断是否要新增子节点。 - 有旧节点需要删除。 如果新节点先patch完了,那么此时会走
newStartIdx > newEndIdx
的逻辑,那么就会去删除多余的旧子节点。
patchVnode的另外三种情况较为简单:
- oldVnode有子节点,Vnode没有子节点
删除oldVnode对应的真实DOM节点的所有子节点
- oldVnode没有子节点,Vnode有子节点
将Vnode的子节点都真实化后添加到oldVnode所对应的真实DOM节点下面
- oldVnode和Vnode都只有文本子节点
如果他们都有文字节点且内容不相等,那么就用Vnode的文字节点值替换oldVnode所对应的真实DOM的文字节点值
拓展: 1、使用了相同的key不一定会复用dom,使用了不同的key肯定会销毁旧的dom创建新的dom产生消耗 2、使用key是为了更加合理的销毁和复用dom 1)合理的销毁:比如在动态表单验证中,如果使用index作为key,会产生删除上面的表单item,但是在使用了index作为key后,会产生错误的复用效果,导致删除后的某些状态依旧停留在下一个item中 2)合理的复用:在一些展示信息list中使用key在数据发生乱序后能够合理的复用dom,减少消耗
Vue3.x与Vue2.x中diff算法
参考文章:Vue3和Vue2的区别 | QT-7274 (qblog.top)
相关文章: