虚拟DOM和DIFF算法


虚拟DOM部分内容详见Vue虚拟DOM详解 | QT-7274 (qblog.top)

diff算法

现在如果要明确两个树结构的差异并进行转换,大致会分为一下两个步骤: 第一阶段:先循环遍历每一个节点进行比较,即针对旧树的每一个节点,都与新树的所有节点进行比较; 第二阶段:在遍历比较结束后,为了做出必要的修改,再次遍历整个树进行实际的操作(添加、删除、修改等)。

代码详解

  1. 初始化一个空数组result,用于存储比较结果。

  2. 定义diff函数,接受两个参数:beforeLeafafterLeaf,分别表示两个要比较的HTML元素树。

  3. 获取两个元素树中较大的子节点数量,并将其赋值给变量count

  4. 使用 for

    循环遍历从0到 count-1 的索引。

    • 如果beforeLeaf的子节点不存在,将afterLeaf的子节点添加到result数组中,类型为"add"。
    • 如果afterLeaf的子节点不存在,将beforeLeaf的子节点从result数组中移除,类型为"remove"。
    • 如果两个子节点的标签名不同,将beforeLeaf的子节点移除,将afterLeaf的子节点添加到result数组中,类型分别为"remove"和"add"。
    • 如果两个子节点的标签名相同但内容不同,进行以下操作:
      • 如果子节点没有子节点,将beforeLeaf的子节点替换为afterLeaf的子节点,类型为"changed",并记录更改后的HTML内容。
      • 如果子节点有子节点,递归调用diff函数进行比较。
  5. 返回存储了所有差异的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代码详解

主要目的是比较新旧虚拟节点(oldVnodevnode),并根据它们之间的差异来更新实际的DOM元素(elm)。

  1. 首先,获取旧虚拟节点的子节点(oldCh)和新虚拟节点的子节点(ch)。

  2. 接下来,检查新虚拟节点(vnode)的文本内容是否为undefined。如果是,说明它有子节点。

  3. 如果新旧虚拟节点都有子节点(oldChch 都不为 undefined),则比较它们的子节点是否相同。如果不同,调用 updateChildren 函数来更新实际DOM元素的子节点。

  4. 如果只有新虚拟节点有子节点(ch 不为 undefinedoldChundefined),首先检查旧虚拟节点是否有文本内容。如果有,清空实际DOM元素的文本内容。然后,使用 addVnodes 函数将新虚拟节点的子节点添加到实际DOM元素中。

  5. 如果只有旧虚拟节点有子节点(oldCh 不为 undefinedchundefined),使用 removeVnodes 函数从实际DOM元素中移除旧虚拟节点的子节点。

  6. 如果旧虚拟节点有文本内容,但新虚拟节点没有子节点,清空实际DOM元素的文本内容。

  7. 如果新旧虚拟节点都只有文本内容,且它们的文本内容不同,使用 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节点的操作。

  1. 旧首节点和新首节点用 sameNode 对比。
  2. 旧尾节点和新尾节点用 sameNode 对比
  3. 旧首节点和新尾节点用 sameNode 对比
  4. 旧尾节点和新首节点用 sameNode 对比
  5. 如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后用新 vnodekey 去找出在旧节点中可以复用的位置。

然后不停的把匹配到的指针向内部收缩,直到新旧节点有一端的指针相遇(说明这个端的节点都被patch过了)。

在指针相遇以后,还有两种比较特殊的情况:

  1. 有新节点需要加入。 如果更新完以后,oldStartIdx > oldEndIdx,说明旧节点都被 patch 完了,但是有可能还有新的节点没有被处理到。接着会去判断是否要新增子节点。
  2. 有旧节点需要删除。 如果新节点先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)

相关文章:


文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录