vuejs 设计与实现 - 双端diff算法

news2024/11/17 0:24:19

我们介绍了简单 Diff 算法的实现原理。简单 Diff 算法利用虚拟节点的 key 属性,尽可能地复用 DOM元素,并通过移动 DOM的方式来完成更新,从而减少不断地创建和销毁 DOM 元素带来的性能开销。但是,简单 Diff 算法仍然存在很多缺陷,这些缺陷可以通过本章将要介绍的双端 Diff 算法解决。

1.双端比较的原理

双端 Diff 算法是一种同时对新旧两组子节点的两个端点进行比较的算法。因此,我们需要四个索引值,分别指向新旧两组子节点的端点.如下图:

请添加图片描述

双端比较的方式:

请添加图片描述
在双端比较中,每一轮比较都分为四个步骤,如图 10-5 中的连线所示。
比较的过程如下描述:
第一步: 比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的第一个子节点 p-4,看看它们是否相同。由于两者的key 值不同,因此不相同,不可复用,于是什么都不做。

第二步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的最后一个子节点 p-3,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。

第三步:比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的最后一个子节点 p-3,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。

第四步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的第一个子节点 p-4。由于它们的 key 值相同,因此可以进行 DOM 复用。
请添加图片描述

 function patchChildren(n1, n2, container) {
   patchKeyedChildren(n1, n2, container)
}

function patchKeyedChildren(n1, n2, container){
   const oldChildren = n1.children 
   const newChildren = n2.children

   // 四个索引值
   let oldStartIdx = 0
   let oldEndIdx = oldChildren.length - 1

   let newStartIdx = 0
   let newEndIdx = newChildren.length - 1

   // 四个索引指向的 vnode 节点
   let oldStartVNode = oldChildren[oldStartIdx]
   let oldEndVNode = oldChildren[oldEndIdx]

   let newStartVNode = newChildren[newStartIdx]
   let newEndVNode = newChildren[newEndIdx]


   if (oldStartVNode.key === newStartVNode.key) {
        // 步骤一:oldStartVNode 和 newStartVNode 比较

    } else if (oldEndVNode.key === newEndVNode.key) {
        // 步骤二:oldEndVNode 和 newEndVNode 比较
     
    } else if(oldStartVNode.key === newEndVNode.key) {
        // 步骤三:oldStartVNode 和 newEndVNode 比较
       


    } else if (oldEndVNode.key === newStartVNode.key) {
        // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
        // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
        // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。


        // 第四步:oldEndVNode 和 newStartVNode 比较
        // 仍然需要调用 patch 函数进行打补丁
        patch(oldEndVNode, newStartVNode, container)

        // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
        insert(oldEndVNode.el, container, oldStartVNode.el)
        
        // 移动 DOM 完成后,更新索引值,指向下一个位置
        oldEndVNode = oldChildren[--oldEndIdx]
        newStartVNode = newChildren[++newStartIdx]
    }


}

第一轮 DOM 移动操作完成 态状的点节后,新旧两组子节点以及真实 DOM 节点的状态如下:
请添加图片描述

此时,真实 DOM 节点顺序为 p-4、p-1、p-2、p-3,这与新的 一组子节点顺序不一致。这是因为diff算法还没结束,还需要进行下一轮更新。因此,我们需要将更新逻辑封装到一个 while 循环中,

function patchChildren(n1, n2, container) {
            patchKeyedChildren(n1, n2, container)
        }

        function patchKeyedChildren(n1, n2, container){
            const oldChildren = n1.children 
            const newChildren = n2.children

            // 四个索引值
            let oldStartIdx = 0
            let oldEndIdx = oldChildren.length - 1

            let newStartIdx = 0
            let newEndIdx = newChildren.length - 1

            // 四个索引指向的 vnode 节点
            let oldStartVNode = oldChildren[oldStartIdx]
            let oldEndVNode = oldChildren[oldEndIdx]

            let newStartVNode = newChildren[newStartIdx]
            let newEndVNode = newChildren[newEndIdx]

 +           while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

                if (oldStartVNode.key === newStartVNode.key) {
                    // 步骤一:oldStartVNode 和 newStartVNode 比较
    
                } else if (oldEndVNode.key === newEndVNode.key) {
                    // 步骤二:oldEndVNode 和 newEndVNode 比较
                 
                } else if(oldStartVNode.key === newEndVNode.key) {
                    // 步骤三:oldStartVNode 和 newEndVNode 比较
                   

    
                } else if (oldEndVNode.key === newStartVNode.key) {
                    // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
                    // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
                    // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。
    
    
                    // 第四步:oldEndVNode 和 newStartVNode 比较
                    // 仍然需要调用 patch 函数进行打补丁
                    patch(oldEndVNode, newStartVNode, container)
    
                    // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
                    insert(oldEndVNode.el, container, oldStartVNode.el)
                    
                    // 移动 DOM 完成后,更新索引值,指向下一个位置
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newStartVNode = newChildren[++newStartIdx]
                }
 +           }


        }

由于在每一轮更新完成之后,紧接着都会更新四个索引中与当前更新轮次相关联的索引,所以整个 while 循环执行的条件是:头部索引值要小于等于尾部索引值。

在第一轮更新结束后循环条件仍然成立,因此需要进行下一轮的比较:

第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-2,看看它们是否相同。由于两者的 key 值不
同,不可复用,所以什么都不做。

这里,我们使用了新的名词: 。它指的是头部索引oldStartIdx 和 newStartIdx 所指向的节点。

第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3,两者的 key 值相同,可以复用。另外,由于两者都处于尾部,因此不需要对真实 DOM 进行移动操作,只需要打补丁即可:


function patchChildren(n1, n2, container) {
            patchKeyedChildren(n1, n2, container)
        }

        function patchKeyedChildren(n1, n2, container){
            const oldChildren = n1.children 
            const newChildren = n2.children

            // 四个索引值
            let oldStartIdx = 0
            let oldEndIdx = oldChildren.length - 1

            let newStartIdx = 0
            let newEndIdx = newChildren.length - 1

            // 四个索引指向的 vnode 节点
            let oldStartVNode = oldChildren[oldStartIdx]
            let oldEndVNode = oldChildren[oldEndIdx]

            let newStartVNode = newChildren[newStartIdx]
            let newEndVNode = newChildren[newEndIdx]

            while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

                if (oldStartVNode.key === newStartVNode.key) {
                    // 步骤一:oldStartVNode 和 newStartVNode 比较
    
                } else if (oldEndVNode.key === newEndVNode.key) {
                    // 步骤二:oldEndVNode 和 newEndVNode 比较
                    // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
      +              patch(oldEndVNode, newEndVNode, container)

                    // 更新索引和头尾部节点变量
       +             oldEndVNode = oldChildren[--oldEndIdx]
       +             newEndVNode = newChildren[--newEndIdx]
    
                } else if(oldStartVNode.key === newEndVNode.key) {
                    // 步骤三:oldStartVNode 和 newEndVNode 比较

    
                } else if (oldEndVNode.key === newStartVNode.key) {
                    // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
                    // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
                    // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。
    
    
                    // 第四步:oldEndVNode 和 newStartVNode 比较
                    // 仍然需要调用 patch 函数进行打补丁
                    patch(oldEndVNode, newStartVNode, container)
    
                    // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
                    insert(oldEndVNode.el, container, oldStartVNode.el)
                    
                    // 移动 DOM 完成后,更新索引值,指向下一个位置
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newStartVNode = newChildren[++newStartIdx]
                }
            }


        }

请添加图片描述
真实 DOM 的顺序相比上一轮没有变化,因为在这一轮的比较中没有对 DOM 节点进行移动,只是对 p-3 节点打补丁。接下来,我们再根据图 上图所示的状态执行下一轮的比较:

第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-2,看看它们是否相同。由于两者的 key 值不
同,不可复用,因此什么都不做。

第二步:比较旧的一组子节点中的尾部节点 p-2 与新的一组子节点中的尾部节点 p-1,看看它们是否相同,由于两者的 key 值不
同,不可复用,因此什么都不做。

第三步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的尾部节点 p-1。两者的 key 值相同,可以复用。

在第三步的比较中,我们找到了相同的节点,这说明: p-1原本是头部节点,但是在新的顺序中,它变成了尾部节点。因此,我们需要将节点p-1对应的真实 DOM 移动到旧的一组子节点的尾部节点 p-2 所对应的真实 DOM 后面,同时还需要更新相应的索引到下一个位置,如图 下图所示:
请添加图片描述

function patchChildren(n1, n2, container) {
            patchKeyedChildren(n1, n2, container)
        }

        function patchKeyedChildren(n1, n2, container){
            const oldChildren = n1.children 
            const newChildren = n2.children

            // 四个索引值
            let oldStartIdx = 0
            let oldEndIdx = oldChildren.length - 1

            let newStartIdx = 0
            let newEndIdx = newChildren.length - 1

            // 四个索引指向的 vnode 节点
            let oldStartVNode = oldChildren[oldStartIdx]
            let oldEndVNode = oldChildren[oldEndIdx]

            let newStartVNode = newChildren[newStartIdx]
            let newEndVNode = newChildren[newEndIdx]

            while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

                if (oldStartVNode.key === newStartVNode.key) {
                    // 步骤一:oldStartVNode 和 newStartVNode 比较
                    // 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁
                   
    
                } else if (oldEndVNode.key === newEndVNode.key) {
                    // 步骤二:oldEndVNode 和 newEndVNode 比较
                    // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
                    patch(oldEndVNode, newEndVNode, container)

                    // 更新索引和头尾部节点变量
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newEndVNode = newChildren[--newEndIdx]
    
                } else if(oldStartVNode.key === newEndVNode.key) {
                    // 步骤三:oldStartVNode 和 newEndVNode 比较
       +             patch(oldStartVNode, newEndVNode, container)
       +             insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

       +             oldStartVNode = oldChildren[++oldStartIdx]
       +             newEndVNode = newChildren[--newEndIdx]

    
                } else if (oldEndVNode.key === newStartVNode.key) {
                    // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
                    // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
                    // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。
    
    
                    // 第四步:oldEndVNode 和 newStartVNode 比较
                    // 仍然需要调用 patch 函数进行打补丁
                    patch(oldEndVNode, newStartVNode, container)
    
                    // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
                    insert(oldEndVNode.el, container, oldStartVNode.el)
                    
                    // 移动 DOM 完成后,更新索引值,指向下一个位置
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newStartVNode = newChildren[++newStartIdx]
                }
            }


        }

下一轮循环:
第一步:比较旧的一组子节点中的头部节点 p-2 与新的一组 子节点中的头部节点 p-2。发现两者 key 值相同,可以复用。但 两者在新旧两组子节点中都是头部节点,因此不需要移动,只需 要调用 patch 函数进行打补丁即可。

function patchChildren(n1, n2, container) {
            patchKeyedChildren(n1, n2, container)
        }

        function patchKeyedChildren(n1, n2, container){
            const oldChildren = n1.children 
            const newChildren = n2.children

            // 四个索引值
            let oldStartIdx = 0
            let oldEndIdx = oldChildren.length - 1

            let newStartIdx = 0
            let newEndIdx = newChildren.length - 1

            // 四个索引指向的 vnode 节点
            let oldStartVNode = oldChildren[oldStartIdx]
            let oldEndVNode = oldChildren[oldEndIdx]

            let newStartVNode = newChildren[newStartIdx]
            let newEndVNode = newChildren[newEndIdx]

            while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

                if (oldStartVNode.key === newStartVNode.key) {
                    // 步骤一:oldStartVNode 和 newStartVNode 比较
                    // 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁
   +                 patch(oldStartVNode, newStartVNode, container)
                    // 更新相关索引,指向下一个位置
   +                 oldStartVNode = oldChildren[++oldStartIdx]
   +                 newStartVNode = newChildren[++newStartIdx]
    
                } else if (oldEndVNode.key === newEndVNode.key) {
                    // 步骤二:oldEndVNode 和 newEndVNode 比较
                    // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
                    patch(oldEndVNode, newEndVNode, container)

                    // 更新索引和头尾部节点变量
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newEndVNode = newChildren[--newEndIdx]
    
                } else if(oldStartVNode.key === newEndVNode.key) {
                    // 步骤三:oldStartVNode 和 newEndVNode 比较
                    patch(oldStartVNode, newEndVNode, container)
                    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

                    oldStartVNode = oldChildren[++oldStartIdx]
                    newEndVNode = newChildren[--newEndIdx]

    
                } else if (oldEndVNode.key === newStartVNode.key) {
                    // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
                    // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
                    // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。
    
    
                    // 第四步:oldEndVNode 和 newStartVNode 比较
                    // 仍然需要调用 patch 函数进行打补丁
                    patch(oldEndVNode, newStartVNode, container)
    
                    // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
                    insert(oldEndVNode.el, container, oldStartVNode.el)
                    
                    // 移动 DOM 完成后,更新索引值,指向下一个位置
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newStartVNode = newChildren[++newStartIdx]
                }
            }


        }

在这一轮更新之后,新旧两组子节点与真实 DOM 节点的状态如图下图 10-10 所示。

请添加图片描述

双端比较的优势

优势:减少移动操作。

案例分析:如下图的新旧两组子节点:
请添加图片描述

简单diff:移动两次
请添加图片描述

双端diff:移动一次

请添加图片描述

非理想状态的处理方式

第一轮都无法命中

  • 旧的一组子节点:p-1、p-2、p-3、p-4。
  • 新的一组子节点:p-2、p-4、p-1、p-3。

当我们尝试按照双端 Diff 算法的思路进行第一轮比较时,会发现无法命中四个步骤中的任何一步。这个时候怎么办呢?这时,我们只能通过增加额外的处理步骤来处理这种非理想情况。既然两个头部和两个尾部的四个节点中都没有可复用的节点,那么我们就尝试看看非头部、非尾部的节点能否复用。具体做法是,拿新的一组子节点中的头部节点去旧的一组子节点中寻找:如下面的代码:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

                if (oldStartVNode.key === newStartVNode.key) {
                    // 步骤一:oldStartVNode 和 newStartVNode 比较
                    // 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁
                    patch(oldStartVNode, newStartVNode, container)
                    // 更新相关索引,指向下一个位置
                    oldStartVNode = oldChildren[++oldStartIdx]
                    newStartVNode = newChildren[++newStartIdx]
    
                } else if (oldEndVNode.key === newEndVNode.key) {
                    // 步骤二:oldEndVNode 和 newEndVNode 比较
                    // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
                    patch(oldEndVNode, newEndVNode, container)

                    // 更新索引和头尾部节点变量
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newEndVNode = newChildren[--newEndIdx]
    
                } else if(oldStartVNode.key === newEndVNode.key) {
                    // 步骤三:oldStartVNode 和 newEndVNode 比较
                    patch(oldStartVNode, newEndVNode, container)
                    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

                    oldStartVNode = oldChildren[++oldStartIdx]
                    newEndVNode = newChildren[--newEndIdx]

    
                } else if (oldEndVNode.key === newStartVNode.key) {
                    // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
                    // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
                    // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。
    
    
                    // 第四步:oldEndVNode 和 newStartVNode 比较
                    // 仍然需要调用 patch 函数进行打补丁
                    patch(oldEndVNode, newStartVNode, container)
    
                    // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
                    insert(oldEndVNode.el, container, oldStartVNode.el)
                    
                    // 移动 DOM 完成后,更新索引值,指向下一个位置
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newStartVNode = newChildren[++newStartIdx]
                } else {
+					// 处理非理想情况

                    // 在旧的一 组子节点中,找到与新的一组子节点的头部节点具有相同 key 值的节点
                    // 遍历旧的一组子节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
                    // idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引
 +                   const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)
				}
            }

如下图在旧子节点中寻找可复用节点:
请添加图片描述

function patchChildren(n1, n2, container) {
            patchKeyedChildren(n1, n2, container)
        }

        function patchKeyedChildren(n1, n2, container){
            const oldChildren = n1.children 
            const newChildren = n2.children

            // 四个索引值
            let oldStartIdx = 0
            let oldEndIdx = oldChildren.length - 1

            let newStartIdx = 0
            let newEndIdx = newChildren.length - 1

            // 四个索引指向的 vnode 节点
            let oldStartVNode = oldChildren[oldStartIdx]
            let oldEndVNode = oldChildren[oldEndIdx]

            let newStartVNode = newChildren[newStartIdx]
            let newEndVNode = newChildren[newEndIdx]

            while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

                if (oldStartVNode.key === newStartVNode.key) {
                    // 步骤一:oldStartVNode 和 newStartVNode 比较
                    // 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁
                    patch(oldStartVNode, newStartVNode, container)
                    // 更新相关索引,指向下一个位置
                    oldStartVNode = oldChildren[++oldStartIdx]
                    newStartVNode = newChildren[++newStartIdx]
    
                } else if (oldEndVNode.key === newEndVNode.key) {
                    // 步骤二:oldEndVNode 和 newEndVNode 比较
                    // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
                    patch(oldEndVNode, newEndVNode, container)

                    // 更新索引和头尾部节点变量
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newEndVNode = newChildren[--newEndIdx]
    
                } else if(oldStartVNode.key === newEndVNode.key) {
                    // 步骤三:oldStartVNode 和 newEndVNode 比较
                    patch(oldStartVNode, newEndVNode, container)
                    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

                    oldStartVNode = oldChildren[++oldStartIdx]
                    newEndVNode = newChildren[--newEndIdx]

    
                } else if (oldEndVNode.key === newStartVNode.key) {
                    // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
                    // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
                    // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。
    
    
                    // 第四步:oldEndVNode 和 newStartVNode 比较
                    // 仍然需要调用 patch 函数进行打补丁
                    patch(oldEndVNode, newStartVNode, container)
    
                    // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
                    insert(oldEndVNode.el, container, oldStartVNode.el)
                    
                    // 移动 DOM 完成后,更新索引值,指向下一个位置
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newStartVNode = newChildren[++newStartIdx]
                }  else {
                    // 处理非理想情况

                    // 在旧的一 组子节点中,找到与新的一组子节点的头部节点具有相同 key 值的节点
                    // 遍历旧的一组子节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
                    // idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引
                    const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)

                    // idxInOld 大于 0,说明找到了可复用的节点,并且需要将其对应的真实DOM 移动到头部
 +                   if(idxInOld > 0) {
 +                       // idxInOld 位置对应的 vnode 就是需要移动的节点
                        const vnodeToMove = oldChildren[idxInOld]
                        
                        // 不要忘记除移动操作外还应该打补丁
 +                       patch(vnodeToMove, newStartVNode, container)

                        // 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点
+                        insert(vnodeToMove.el, container, oldStartVNode.el)

                        // 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处,因此将其设置为 undefined
 +                       oldChildren[idxInOld] = undefined
                        
                        // 最后更新 newStartIdx 到下一个位置
 +                       newStartVNode = newChildren[++newStartIdx]
                    }
                }
            }


        }

在上面这段代码中,首先判断 idxInOld 是否大于 0。如果条件 成立,则说明找到了可复用的节点,然后将该节点对应的真实 DOM 移 动到头部。为此,我们先要获取需要移动的节点,这里的 oldChildren[idxInOld] 所指向的节点就是需要移动的节点。在移 动节点之前,不要忘记调用 patch 函数进行打补丁。接着,调用 insert 函数,并以现在的头部节点对应的真实 DOM 节点 oldStartVNode.el 作为锚点参数来完成节点的移动操作。当节点移 动完成后,还有两步工作需要做:

    1. 由于处于 idxInOld 处的节点已经处理过了(对应的真实 DOM 移到了别处),因此我们应该将 oldChildren[idxInOld] 设 置为undefined。
    1. 新的一组子节点中的头部节点已经处理完毕,因此将 newStartIdx 前进到下一个位置。

经过上述两个步骤的操作后,新旧两组子节点以及真实 DOM 节点 的状态如图 下图所示:
请添加图片描述
此时,真实 DOM 的顺序为:p-2、p-1、p-3、p-4。接着,双端 Diff 算法会继续进行。如下图所示:
请添加图片描述

第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4,两者 key 值不同,不可复用。
第二步:比较旧的一组子节点中的尾部节点 p-4 与新的一组子节点中的尾部节点 p-3,两者 key 值不同,不可复用。
第三步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的尾部节点 p-3,两者 key 值不同,不可复用。
第四步:比较旧的一组子节点中的尾部节点 p-4 与新的一组子节点中的头部节点 p-4,两者的 key 值相同,可以复用。

在这一轮比较的第四步中,我们找到了可复用的节点。因此,按照双端 Diff 算法的逻辑移动真实 DOM,即把节点 p-4 对应的真实DOM 移动到旧的一组子节点中头部节点 p-1 所对应的真实 DOM 前面,如图 下图 所示:

请添加图片描述

此时,真实 DOM 节点的顺序是:p-2、p-4、p-1、p-3。接着,开始下一轮的比较:
第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-1,两者的 key 值相同,可以复用。

在这一轮比较中,第一步就找到了可复用的节点。由于两者都处于头部,所以不需要对真实 DOM 进行移动,只需要打补丁即可。在这一步操作过后,新旧两组子节点与真实 DOM 节点的状态如图 下图 所示:
请添加图片描述

此时,真实 DOM 节点的顺序是:p-2、p-4、p-1、p-3。接着,进行下一轮的比较。需要注意的一点是,此时旧的一组子节点的
头部节点是 undefined。这说明该节点已经被处理过了,因此不需要再处理它了,直接跳过即可。为此,我们需要补充这部分逻辑的代码,具体实现如下:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

	// 增加两个判断分支,如果头尾部节点为 undefined,则说明该节点已经被处理过了,直接跳到下一个位置
   + if (!oldStartVNode) {
   +     oldStartVNode = oldChildren[++oldStartIdx]
   + } else if (!oldEndVNode) {
   + 	 oldEndVNode = oldChildren[--oldEndIdx]
   + }else if (oldStartVNode.key === newStartVNode.key) {
        // 步骤一:oldStartVNode 和 newStartVNode 比较
        // 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁
        patch(oldStartVNode, newStartVNode, container)
        // 更新相关索引,指向下一个位置
        oldStartVNode = oldChildren[++oldStartIdx]
        newStartVNode = newChildren[++newStartIdx]

    } else if (oldEndVNode.key === newEndVNode.key) {
        // 步骤二:oldEndVNode 和 newEndVNode 比较
        // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
        patch(oldEndVNode, newEndVNode, container)

        // 更新索引和头尾部节点变量
        oldEndVNode = oldChildren[--oldEndIdx]
        newEndVNode = newChildren[--newEndIdx]

    } else if(oldStartVNode.key === newEndVNode.key) {
        // 步骤三:oldStartVNode 和 newEndVNode 比较
        patch(oldStartVNode, newEndVNode, container)
        insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

        oldStartVNode = oldChildren[++oldStartIdx]
        newEndVNode = newChildren[--newEndIdx]


    } else if (oldEndVNode.key === newStartVNode.key) {
        // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
        // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
        // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。


        // 第四步:oldEndVNode 和 newStartVNode 比较
        // 仍然需要调用 patch 函数进行打补丁
        patch(oldEndVNode, newStartVNode, container)

        // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
        insert(oldEndVNode.el, container, oldStartVNode.el)
        
        // 移动 DOM 完成后,更新索引值,指向下一个位置
        oldEndVNode = oldChildren[--oldEndIdx]
        newStartVNode = newChildren[++newStartIdx]
    }  else {
        // 处理非理想情况

        // 在旧的一 组子节点中,找到与新的一组子节点的头部节点具有相同 key 值的节点
        // 遍历旧的一组子节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
        // idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引
        const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)

        // idxInOld 大于 0,说明找到了可复用的节点,并且需要将其对应的真实DOM 移动到头部
        if(idxInOld > 0) {
            // idxInOld 位置对应的 vnode 就是需要移动的节点
            const vnodeToMove = oldChildren[idxInOld]
            
            // 不要忘记除移动操作外还应该打补丁
            patch(vnodeToMove, newStartVNode, container)

            // 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点
            insert(vnodeToMove.el, container, oldStartVNode.el)

            // 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处,因此将其设置为 undefined
            oldChildren[idxInOld] = undefined
            
            // 最后更新 newStartIdx 到下一个位置
            newStartVNode = newChildren[++newStartIdx]
        }
    }
}

观察上面的代码,在循环开始时,我们优先判断头部节点和尾部节点是否存在。如果不存在,则说明它们已经被处理过了,直接跳到下一个位置即可。在这一轮比较过后,新旧两组子节点与真实 DOM 节点的状态如图 下图 所示:
请添加图片描述

现在,四个步骤又重合了,接着进行最后一轮的比较:
第一步:比较旧的一组子节点中的头部节点 p-3 与新的一组子节点中的头部节点 p-3,两者的 key 值相同,可以复用。在第一步中找到了可复用的节点。由于两者都是头部节点,因此不需要进行 DOM 移动操作,直接打补丁即可。在这一轮比较过后,最终状态如图 下图 所示:
请添加图片描述
这时,满足循环停止的条件,于是更新完成。最终,真实 DOM 节点的顺序与新的一组子节点的顺序一致,都是:p-2、p-4、p-1、p-3。

添加新元素

添加新元素的时机:1.四个步骤的比较中都找不到可复用的节点 。 2.尝试拿新的一组子节点中的头部节点 p-4 去旧的一组子节点中寻找具有相同 key 值的节点,但在旧的一组子节点中根本就没有 p-4 节点。这说明节点 p-4 是一个新增节点。

案例1如下:

  • 旧的一组子节点:p-1、p-2、p-3。
  • 新的一组子节点:p-4、p-1、p-3、p-2。
    请添加图片描述

代码如下:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
                if (!oldStartVNode) {
                    oldStartVNode = oldChildren[++oldStartIdx]
                } else if (oldStartVNode.key === newStartVNode.key) {
                    // 步骤一:oldStartVNode 和 newStartVNode 比较
                    // 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁
                    patch(oldStartVNode, newStartVNode, container)
                    // 更新相关索引,指向下一个位置
                    oldStartVNode = oldChildren[++oldStartIdx]
                    newStartVNode = newChildren[++newStartIdx]
    
                } else if (oldEndVNode.key === newEndVNode.key) {
                    // 步骤二:oldEndVNode 和 newEndVNode 比较
                    // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
                    patch(oldEndVNode, newEndVNode, container)

                    // 更新索引和头尾部节点变量
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newEndVNode = newChildren[--newEndIdx]
    
                } else if(oldStartVNode.key === newEndVNode.key) {
                    // 步骤三:oldStartVNode 和 newEndVNode 比较
                    patch(oldStartVNode, newEndVNode, container)
                    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

                    oldStartVNode = oldChildren[++oldStartIdx]
                    newEndVNode = newChildren[--newEndIdx]

    
                } else if (oldEndVNode.key === newStartVNode.key) {
                    // 我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。
                    // 于是,我们只需要以头部元素oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。
                    // 但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。
    
    
                    // 第四步:oldEndVNode 和 newStartVNode 比较
                    // 仍然需要调用 patch 函数进行打补丁
                    patch(oldEndVNode, newStartVNode, container)
    
                    // 移动dom操作  oldEndVNode.el 移动到 oldStartVNode.el 前面
                    insert(oldEndVNode.el, container, oldStartVNode.el)
                    
                    // 移动 DOM 完成后,更新索引值,指向下一个位置
                    oldEndVNode = oldChildren[--oldEndIdx]
                    newStartVNode = newChildren[++newStartIdx]
                }  else {
                    // 处理非理想情况

                    // 在旧的一 组子节点中,找到与新的一组子节点的头部节点具有相同 key 值的节点
                    // 遍历旧的一组子节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
                    // idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引
                    const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)

                    // idxInOld 大于 0,说明找到了可复用的节点,并且需要将其对应的真实DOM 移动到头部
                    if(idxInOld > 0) {
                        // idxInOld 位置对应的 vnode 就是需要移动的节点
                        const vnodeToMove = oldChildren[idxInOld]
                        
                        // 不要忘记除移动操作外还应该打补丁
                        patch(vnodeToMove, newStartVNode, container)

                        // 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点
                        insert(vnodeToMove.el, container, oldStartVNode.el)

                        // 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处,因此将其设置为 undefined
                        oldChildren[idxInOld] = undefined
                        
                        // 最后更新 newStartIdx 到下一个位置
                        newStartVNode = newChildren[++newStartIdx]
                    } else {

+          				// 新增节点
+           			// 将 newStartVNode 作为新节点挂载到头部,使用当前头部节点oldStartVNode.el 作为锚点
+           			patch(null, newStartVNode, container, oldStartVNode.el)
                    }
                }
            }

当条件idxInOld > 0不成立时,说明 newStartVNode 节点是全新的节点。又由于 newStartVNode 节点 是头部节点,因此我们应该将其作为新的头部节点进行挂载。所以, 在调用 patch 函数挂载节点时,我们使用 oldStartVNode.el 作为 锚点。在这一步操作完成之后,新旧两组子节点以及真实 DOM 节点的 状态如下图所示:
请添加图片描述

案例2

  • 旧的一组子节点:p-1、p-2、p-3。
  • 新的一组子节点:p-4、p-1、p-2、p-3。
    第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4,两者的 key 值不同,不可以复用。
    第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3,两者的 key 值相同,可以复用。
    在第二步中找到了可复用的节点,因此进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图下图 所示:
    请添加图片描述
    接着进行下一轮的比较:
    第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4,两者的 key 值不同,不可以复用。
    第二步:比较旧的一组子节点中的尾部节点 p-2 与新的一组子节点中的尾部节点 p-2,两者的 key 值相同,可以复用。
    我们又在第二步找到了可复用的节点,于是再次进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图 所示:

请添加图片描述
接着,进行下一轮的更新:
第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4,两者的 key 值不同,不可以复用。
第二步:比较旧的一组子节点中的尾部节点 p-1 与新的一组子节点中的尾部节点 p-1,两者的 key 值相同,可以复用。

还是在第二步找到了可复用的节点,再次进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图 所示:
请添加图片描述

当这一轮更新完毕后,由于变量 oldStartIdx 的值大于oldEndIdx 的值,满足更新停止的条件,因此更新停止。但通过观察可知,节点 p-4 在整个更新过程中被遗漏了,没有得到任何处理,这说明我们的算法是有缺陷的。为了弥补这个缺陷,我们需要添加额外的处理代码:

 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 
  // 省略部分代码
 }
 // 循环结束后检查索引值的情况,
+  if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
+ 	// 如果满足条件,则说明有新的节点遗留,需要挂载它们
+ for (let i = newStartIdx; i <= newEndIdx; i++) {
+      patch(null, newChildren[i], container, oldStartVNode.el)


 +  }
 }

我们在 while 循环结束后增加了一个 if 条件语句,检查四个索引值的情况。根据图上图可知,如果条件oldEndIdx <oldStartIdx && newStartIdx <= newEndIdx成立,说明新的一组子节点中有遗留的节点需要作为新节点挂载。哪些节点是新节点呢?索引值位于 newStartIdx 和 newEndIdx 这个区间内的节点都是新节点。``于是我们开启一个 for 循环来遍历这个区间内的节点并逐一挂载。挂载时的锚点仍然使用当前的头部节点oldStartVNode.el,这样就完成了对新增元素的处理。

移除不存在的元素

案例如下:

  • 旧的一组子节点:p-1、p-2、p-3。
  • 新的一组子节点:p-1、p-3。
    请添加图片描述

可以看到,在新的一组子节点中 p-2 节点已经不存在了。为了搞清楚应该如何处理节点被移除的情况,我们还是按照双端 Diff 算法的思路执行更新。
第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-1,两者的 key 值相同,可以复用。
在第一步的比较中找到了可复用的节点,于是执行更新。在这一轮比较过后,新旧两组子节点以及真实 DOM 节点的状态如图下图所示:
请添加图片描述
接着,执行下一轮更新:
第一步:比较旧的一组子节点中的头部节点 p-2 与新的一组子节点中的头部节点 p-3,两者的 key 值不同,不可以复用。
第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3,两者的 key 值相同,可以复用。

在第二步中找到了可复用的节点,于是进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图所示:
请添加图片描述
此时变量 newStartIdx 的值大于变量 newEndIdx 的值,满足更新停止的条件,于是更新结束。但观察图 10-34 可知,旧的一组子节点中存在未被处理的节点,应该将其移除。因此,我们需要增加额外的代码来处理它,如下所示:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 
  // 省略部分代码
 }
 // 循环结束后检查索引值的情况,
if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
 	// 如果满足条件,则说明有新的节点遗留,需要挂载它们
 for (let i = newStartIdx; i <= newEndIdx; i++) {
      patch(null, newChildren[i], container, oldStartVNode.el)


   }
+ } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
+ 	 for (let i = oldStartIdx; i <= oldEndIdx; i++) {
+		unmount(oldChildren[i])
+	}
 }

与处理新增节点类似,我们在 while 循环结束后又增加了一个else…if 分支,用于卸载已经不存在的节点。由图 上图 可知,索引值位于 oldStartIdx 和 oldEndIdx 这个区间内的节点都应该被卸载,于是我们开启一个 for 循环将它们逐一卸载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/853142.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

设备使用RTMP推流到安防监控EasyCVR视频汇聚平台,为何只有FLV格式无法播放?

TSINGSEE青犀视频安防监控视频汇聚平台EasyCVR基于云边端一体化架构&#xff0c;具有强大的数据接入、处理及分发能力&#xff0c;可提供视频监控直播、云端录像、云存储、录像检索与回看、智能告警、平台级联、云台控制、语音对讲、智能分析等功能。 智能视频监控平台EasyCVR可…

镭速-解析极速文件传输软件

每天都要传输文件&#xff0c;让人心烦意乱&#xff0c;如果有一种最快的文件传输软件&#xff0c;就能节约很多时间&#xff0c;那么针对用户的这个需求&#xff0c;我们来介绍一下镭速的文件传输软件&#xff0c;看它是否真的那么快&#xff0c;快得让你惊讶。 文件传输要怎么…

vue 09 多组件项目 todolist ,组件编程三部曲,函数方式传递消息,ref,自定义事件传递消息

分析项目组件构成&#xff1a; 组件编程三部曲&#xff1a; 第一步.先创建四个需要用到的组件 由于header&#xff0c;footer&#xff0c;list&#xff0c;item都和html文件或者是js的关键字有冲突&#xff0c;所以建议改成两个单词的名字&#xff0c;MyHeader&#xff0c;My…

2542. 最大子序列的分数

题目描述&#xff1a; 主要思路&#xff1a; 这是一个堆的题目&#xff0c;首先将2里边的下标按照数值递减的顺序进行重新排列。依次遍历2的下标同时加上1的数值&#xff0c;堆里边存储1的大小。 class Solution { public:long long maxScore(vector<int>& nums1, …

Leetcode | 有效的括号、最长有效括号

一、有效的括号 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应…

用node.js搭建一个视频推流服务

由于业务中有不少视频使用的场景&#xff0c;今天来说说如何使用node完成一个视频推流服务。 先看看效果&#xff1a; 这里的播放的视频是一个多个Partial Content组合起来的&#xff0c;每个Partial Content大小是1M。 一&#xff0c;项目搭建 &#xff08;1&#xff09;初…

可靠传输-回退N帧协议

上图中&#xff0c;第一幅图为停止等待协议&#xff0c;而第二幅图为流水线传输&#xff0c;是在接收方返回确认收到的数据之前发送N个数据分组。 回退N帧协议GBN&#xff08;go-back-N&#xff09;&#xff1a; 无差错情况&#xff1a; 1.采用3个比特给分组编号&#xff0c…

从小白到大神之路之学习运维第76天-------Kubernetes工作原理(k8s)

第四阶段 时 间&#xff1a;2023年8月9日 参加人&#xff1a;全班人员 内 容&#xff1a; Kubernetes工作原理 目录 一、案例概述 传统部署时代&#xff1a; 虚拟化部署时代&#xff1a; 容器部署时代&#xff1a; 二、Kubernetes概述 &#xff08;一&#xff…

Pytorch Tutorial【Chapter 3. Simple Neural Network】

Pytorch Tutorial【Chapter 3. Simple Neural Network】 文章目录 Pytorch Tutorial【Chapter 3. Simple Neural Network】Chapter 3. Simple Neural Network3.1 Train Neural Network Procedure训练神经网络流程3.2 Build Neural Network Procedure 搭建神经网络3.3 Use Loss …

机器学习、深度学习项目开发业务数据场景梳理汇总记录二

本文的主要作用是对历史项目开发过程中接触到的业务数据进行整体的汇总梳理&#xff0c;文章会随着项目的开发推进不断更新。 这里是续文&#xff0c;因为CSDN单篇文章内容太大的话就会崩溃的&#xff0c;别问我怎么知道的&#xff0c;问就是血泪教训&#xff0c;辛辛苦苦写了一…

直播预告 | 全面解读现代发布/订阅模型

线上沙龙-技术流第 34 期营业啦 08月10日&#xff08;周四&#xff09;19:30 KaiwuDB - B站直播间 PubSub 模式在 IoT 场景下非常常见&#xff0c;可以灵活地在线配置采集数据的流向。同时 PubSub 可以使系统容易解耦&#xff0c;增加系统的横向扩展性。 本期直播我们邀请到…

8月第1周榜单丨飞瓜数据B站UP主排行榜(哔哩哔哩平台)发布!

飞瓜轻数发布2023年7月31日-8月06日飞瓜数据UP主排行榜&#xff08;B站平台&#xff09;&#xff0c;通过充电数、涨粉数、成长指数三个维度来体现UP主账号成长的情况&#xff0c;为用户提供B站号综合价值的数据参考&#xff0c;根据UP主成长情况用户能够快速找到运营能力强的B…

tft_espi 中文字体及自定义字体制作(tft.eSPI模块)

tft.eSPI库自定义字体方法 先下载 processing 然后安装 到mixly安装目录下找到tft_eSPI文件夹 路径如&#xff1a;D:\Mixly1.20\arduino\portable\sketchbook\libraries\TFT_eSPI\Tools 将Tools文件夹单独复制出来如我的为 E:\tft_espi字体制作Tools 在tft_espi字体制作To…

【香瓜说职场】建立公司(2017.07.25)

自从17年4月份开始辞职创业&#xff0c;已经3个多月了。跟大家分享一下创业经历。 一、我的合伙人 我的合伙人是我的客户。我给他上过蓝牙教学课、帮他做了个蓝牙自拍器&#xff08;叫orbit 360&#xff0c;已在售&#xff0c;百度能搜到&#xff09;&#xff0c;历经一年多。双…

今天学前端,还能高薪就业吗?

大学毕业3年后&#xff0c;我坚定的选择来黑马转行学前端&#xff0c;实现我的高起点就业&#xff01;希望我的一些学习和工作感悟能对学弟学妹们有所帮助。 学科 | HTML&JS前端 校区 | 武汉 薪资 | 12k 黑马程序员的学弟、学妹们大家好&#xff01;我是张同学。 选择黑…

vue基础-vue开发如何调试

文章目录 前言一、debugger二、Vue.js devtools总结 前言 对于一个前端小白来说&#xff0c;在vue项目开发过程中&#xff0c;当遇到应用逻辑出现错误&#xff0c;使用好调试工具更能准确定位到问题。所以知晓Vue项目调试技巧至关重要&#xff0c;debug是必备技能。 下面介绍…

负责任的训练数据:三个重要方面

毫无疑问&#xff0c;人工智能&#xff08;Artificial Intelligence&#xff09;技术一定会在接下来的几年中持续快速发展&#xff0c;并与我们的日常生活愈发密切地联系在一起。现在&#xff0c;企业必须要承担起责任&#xff0c;实施负责任的AI&#xff0c;以最大限度地提高透…

MySQL的第一篇文章——了解数据库、简单的SQL语句

目录 学习目标 第一章 介绍数据库 1. 数据库概述 2. MySQL概述 第二章 MySQL的使用 1. MySQL服务的启动 2. 客户端连接MySQL 2.1 命令行客户端 第三章 SQL的介绍 1. 什么是SQL 2. SQL的分类 3. MySQL的语法规范和要求 第四章 DDL操作数据库 1. 创建数据库 2. 查…

《使用 VMware 在 Windows 上搭建 Linux 系统的完整指南》

《使用 VMware 在 Windows 上搭建 Linux 系统的完整指南》 1、准备工作1.1 安装 VMware 软件1.2 下载 Linux 发行版镜像文件1.3 安装SSH工具 2、创建新的虚拟机2.1 VMware页面2.2 打开VMware页面并点击创建新的虚拟机&#xff0c;选择自定义2.3 选择系统兼容性&#xff0c;默认…

PHP Mysql查询全部全部返回字符串类型

设置pdo属性 $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);