Vue渲染器(五):快速diff算法

news2025/2/23 5:26:02

渲染器(五):快速diff算法

这章开始讨论第三种用于比较新旧vnode的方式:快速diff算法。跟它的名字一样,它很快。Vue3采用的就是这种算法,Vue2采用的是上一章中的双端diff算法。

接下来就来着重了解它的实现思路。

1.相同的前置元素和后置元素:

不同于简单diff算法和双端diff算法,快速Diff算法包含预处理步骤。这其实借鉴了纯文本diff算法的思路,即在对两段文本进行diff之前,先对它们进行全等比较:

if (text1 === text2) return;

如果全等,则无需进入核心diff算法。除此之外,预处理过程还会处理两段文本相同的前缀和后缀,如以下两段文本:

TEXT1: I use vue for app development 
TEXT2: I use react for app development

可以发现,除了第三个字符串vue 和 react不同,其它都一样。因此对于这两段文本,需要进行diff操作的部分是:TEXT1: vueTEXT2: react。这实际上是简化问题的一种方式。

这样子就能够轻松判断文本的插入和删除了。

例如插入操作,去除掉相同前缀内容后:

TEXT1: I like you
TEXT2: I like you too

TEXT1:
TEXT2: too

例如删除操作:

TEXT1: I like you too
TEXT2: I like you

TEXT1: too
TEXT2: 

来看diff算法是如何做的,以下图中的新旧vnode为例:
在这里插入图片描述

它具有相同的前置节点p-1 和 相同的后置节点 p-2 和p-3。对于相同的前置节点和后置节点,由于它们在新旧vnode中的相对位置不变,所以不需要移动,但仍然需要进行打补丁操作。

  • 前置节点更新的处理:通过建立索引j,初始值为0,用来指向新旧vnode节点的开头,然后while循环向后遍历,让索引j自增,直到遇到拥有不同key值的节点为止。如图所示:在这里插入图片描述

  • 后置节点更新的处理:建立两个索引指向新旧vnode节点中的最后一个节点(因为新旧vnode子节点的数量可能不同,故需要两个索引),然后while循环从后向前遍历,直到遇到拥有不同key值的节点为止,如图所示:在这里插入图片描述

实现代码如下:

function patchKeyedChildren(n1, n2, container) {
    // 处理前置节点
    const oldChildren = n1.children;
    const newChildren = n2.children;
    let j = 0;
    let oldVnode = oldChildren[j];
    let newVnode = newChildren[j];
    // while循环向后遍历,直到遇到拥有不同key值的节点为止
    while (oldVnode.key === newVnode.key) {
        patch(oldVnode, newVnode, container);
        j++;
        oldVnode = oldChildren[j];
        newVnode = newChildren[j];
    }

    // 处理后置节点
    let oldEnd = oldChildren.length - 1;
    let newEnd = newChildren.length - 1;
    oldVnode = oldChildren[oldEnd];
    newVnode = newChildren[newEnd];
    // while循环从后向前遍历,直到遇到拥有不同key值的节点为止
    while (oldVnode.key === newVnode.key) {
        patch(oldVnode, newVnode, container);
        oldEnd--;
        newEnd--;
        oldVnode = oldChildren[oldEnd];
        newVnode = newChildren[newEnd];
    }
}

在这一步更新操作过后,新旧vnode此时的状态如图所示:在这里插入图片描述

观察上图,可以发现还遗留了一个未被挂载的新增节点p-4。那么代码该怎么写呢?观察三个索引之间的关系:

  • 条件一:oldEnd < j成立,说明在预处理过程中,所有旧vnode已处理完毕;
  • 条件二:newEnd >= j成立,说明还有未被处理的新增节点。

两个条件成立,即说明需要执行挂载操作,并挂载到正确位置。就是说得把新增vnode挂载到节点 p-2所对应的真实DOM前面。所以节点p-2对应的真实DOM节点就是挂载操作的锚点元素。

代码实现:看新增加的if判断即可

function patchKeyedChildren(n1, n2, container) {
    // 处理前置节点
    const oldChildren = n1.children;
    const newChildren = n2.children;
    let j = 0;
    let oldVnode = oldChildren[j];
    let newVnode = newChildren[j];
    // while循环向后遍历,直到遇到拥有不同key值的节点为止
    while (oldVnode.key === newVnode.key) {
        patch(oldVnode, newVnode, container);
        j++;
        oldVnode = oldChildren[j];
        newVnode = newChildren[j];
    }

    // 处理后置节点
    let oldEnd = oldChildren.length - 1;
    let newEnd = newChildren.length - 1;
    oldVnode = oldChildren[oldEnd];
    newVnode = newChildren[newEnd];
    // while循环从后向前遍历,直到遇到拥有不同key值的节点为止
    while (oldVnode.key === newVnode.key) {
        patch(oldVnode, newVnode, container);
        oldEnd--;
        newEnd--;
        oldVnode = oldChildren[oldEnd];
        newVnode = newChildren[newEnd];
    }
    
    // 预处理完毕后,如果满足如下条件,说明还有未被挂载的新增节点
    if (j > oldEnd && j <= newEnd) {
        // 锚点索引
        const anchorIndex = newEnd + 1;
        // 锚点元素
        const anchor = anchorIndex < newChildren.length
            ? newChildren[anchorIndex].el : null;
        // 采用while循环,逐个挂载新增节点
        while (j <= newEnd) {
            patch(null, newChildren[j++], container, anchor);
        }
    }
}

代码解释都写在注释里了,接下来来看看删除节点的情况:

当新vnode的数量小于旧vnode时,就会出现需要删除节点的情况。举例:在这里插入图片描述

对相同的前置节点和后置节点进行预处理后,此时的状态如图所示:

在这里插入图片描述

索引j和索引oldEnd之间的任何节点都应该被卸载,具体实现如下:

    // 预处理完毕后,如果满足如下条件,说明还有未被挂载的新增节点
    if (j > oldEnd && j <= newEnd) {
        // 锚点索引
        const anchorIndex = newEnd + 1;
        // 锚点元素
        const anchor = anchorIndex < newChildren.length
            ? newChildren[anchorIndex].el : null;
        // 采用while循环,逐个挂载新增节点
        while (j <= newEnd) {
            patch(null, newChildren[j++], container, anchor);
        }
    } else if (j > newEnd && j <= oldEnd) {
        // 预处理完毕后,如果满足如下条件,说明还有未被卸载的旧节点
        while (j <= oldEnd) {
            unmount(oldChildren[j++]);
        }
    }

在前面代码上新增加的代码,多了一个 else...if 分支。解释都写在注释里啦。

2.判断是否需要进行DOM移动操作:

在上一节中,讲解了快速diff算法的预处理过程,即处理相同前置节点和后置节点。但是有时情况可能比较复杂, 如下图:在这里插入图片描述

它只有少量的前置、后置节点,并且新vnode中多了p-7,少了p-6。

经过预处理后,无论是新旧vnode,都有部分节点未经处理,这时就需要进一步处理。

那么该怎么处理呢?其实无论是简单diff算法、双端diff算法,还是快速diff算法,它们都遵守同样的处理规则:

  • 判断是否有节点需要移动,以及应该如何移动;
  • 找出那些需要被添加或移除的节点。

那该怎么判断呢?其实在前面的基础上多添加一个else分支即可,因为三个索引都不满足下面两个条件中的任何一个:

  • j > oldEnd && j <= newEnd
  • j > newEnd && j <= oldEnd

具体处理思路:先构造一个source数组,它的长度等于新vnode在经过预处理后剩余未处理节点的数量,并且source中每个元素的初始值都是 -1 。
在这里插入图片描述

代码实现:看else分支即可

    // 预处理完毕后,如果满足如下条件,说明还有未被挂载的新增节点
    if (j > oldEnd && j <= newEnd) {
        // 锚点索引
        const anchorIndex = newEnd + 1;
        // 锚点元素
        const anchor = anchorIndex < newChildren.length
            ? newChildren[anchorIndex].el : null;
        // 采用while循环,逐个挂载新增节点
        while (j <= newEnd) {
            patch(null, newChildren[j++], container, anchor);
        }
    } else if (j > newEnd && j <= oldEnd) {
        // 预处理完毕后,如果满足如下条件,说明还有未被卸载的旧节点
        while (j <= oldEnd) {
            unmount(oldChildren[j++]);
        }
    } else {
        // 构造 source数组,保存新vnode中剩余未处理节点的数量
        const count = newEnd - j + 1;
        const source = new Array(count);
        source.fill(-1);
    }

数组source的作用:如上图所示,每个元素分别与新vnode中剩余未处理节点一一对应。

实际上,source 数组将用来存储新vnode在旧vnode中的位置索引,后面将使用它计算出一个最长递增子序列,并用于辅助完成DOM移动的操作。如下图展示了填充source数组的过程:在这里插入图片描述

介绍一下用双for循环的source数组填充实现:

else {
        // 构造 source数组,保存新vnode中剩余未处理节点的数量
        const count = newEnd - j + 1;
        const source = new Array(count);
        source.fill(-1);
		
		// 起始索引
        const oldStart = j;
        const newStart = j;
        // 遍历旧vnode
        for (let i = oldStart; i <= oldEnd; i++) {
            const oldVnode = oldChildren[i];
            // 遍历新vnode
            for (let k = newStart; k <= newEnd; k++) {
                const newVnode = newChildren[k];
                if (oldVnode.key === newVnode.key) {
                    patch(oldVnode, newVnode, container);
                    // 由于数组source的索引从开始,未处理节点的索引未必从0开始
                    // 所以填充时需要用 k - newStart来表示数组的索引值
                    source[k - newStart] = i;
                }
            }
        }
    }

用双for循环嵌套的时间复杂度有点高,可能会带来性能问题,所以需要进行优化。为新vnode构建一张索引表,用来存储节点key和节点位置索引之间的映射,如下图:
在这里插入图片描述

代码实现:

else {
        // 构造 source数组,保存新vnode中剩余未处理节点的数量
        const count = newEnd - j + 1;
        const source = new Array(count);
        source.fill(-1);

        // 起始索引
        const oldStart = j;
        const newStart = j;
        // 构建索引表
        const keyIndex = {};
        for (let i = newStart; i <= newEnd; i++) {
            keyIndex[newChildren[i].key] = i;
        }
        // 遍历旧vnode中剩余未处理的节点
        for (let i = oldStart; i <= oldEnd; i++) {
            oldVnode = oldChildren[i];
            // 通过索引表快速找到新vnode中具有相同key值的节点位置
            const k = keyIndex[oldVnode.key];
            if (typeof k !== 'undefined') {
                newVnode = newChildren[k];
                patch(oldVnode, newVnode, container);
                // 填充source数组
                source[k - newStart] = i;
            } else {
                // 没找到
                unmount(oldVnode);
            }
        }
    }

代码解释:

  • 第一个for循环用来构建索引表,索引表:存储vnode的key值 和 在新vnode中位置索引的映射。
  • 第二个for循环用来遍历旧vnode,并且用旧vnode中的key值去索引表中查找该节点在新vnode中具有相同key值的节点位置(因为新旧vnode的key相同)。存储到变量k中,如果k存在,说明该节点是可复用的,然后进行source数组填充。

source数组填充完了,接下来该思考如何判断节点是否需要移动?

  • 通过新增两个变量moved和pos。前者初始值为false代表是否需要移动节点,后者初始值为0,代表遍历旧vnode的过程中遇到的最大索引值k。
  • 在前面的简单diff算法时提到,如果在遍历过程中遇到的索引值呈递增趋势,说明不需要移动节点;反之需要。
  • 所以在第二个for循环内通过 比较变量k与变量pos的值来判断是否需要移动节点。

除此之外,还需要一个数量标识来代表已经更新过的节点数量。已经更新过的vnode数量应该小于新vnode中需要更新的节点数量。否则说明有多余节点,需要将其卸载。此时的代码:

else {
        // 构造 source数组,保存新vnode中剩余未处理节点的数量
        const count = newEnd - j + 1;
        const source = new Array(count);
        source.fill(-1);

        // 起始索引
        const oldStart = j;
        const newStart = j;
        // 新增两个变量moved和pos
        let moved = false;
        let pos = 0;
        // 构建索引表
        const keyIndex = {};
        for (let i = newStart; i <= newEnd; i++) {
            keyIndex[newChildren[i].key] = i;
        }
        // 新增 patched变量,代表更新过的节点数量
        let patched = 0;
        for (let i = oldStart; i <= oldEnd; i++) {
            oldVnode = oldChildren[i];
            // 如果更新过的vnode数量 小于 需要更新的vnode数量,则执行更新
            if (patched <= count) {
                const k = keyIndex[oldVnode.key];
                if (typeof k !== 'undefined') {
                    newVnode = newChildren[k];
                    patch(oldVnode, newVnode, container);
                    source[k - newStart] = i;
                    // 判断节点是否需要移动
                    if (k < pos) {
                        moved = true;
                    } else {
                        pos = k;
                    }
                } else {
                    // 没找到
                    unmount(oldVnode);
                }
            } else {
                // 如果更新过的vnode数量 大于 需要更新的vnode数量,则卸载多余节点
                unmount(oldVnode);
            }
        }
    }

3.如何移动元素?

如何判断是否需要进行DOM移动操作?

  • 当moved值为true时,说明需要进行DOM移动操作

该如何移动元素?

  • source数组中保存着剩余未处理节点数量,里面存储着新vnode的节点在旧vnode中的位置。根据source数组计算出一个最长递增子序列,用于DOM移动操作。

啥是最长递增子序列?

  • 假如给定序列中 [3,5,4,7],那么它的最长递增子序列就是 [3,5,7]或者[3,4,7]。它的最长子序递增子序列可能有多个,需要注意的是后面它返回的结果是最长递增子序列中的元素在source数组中的位置索引。如上面例子中的 [0,1,3] 或 [0,2,3]

了解完上面的后,回归前面的例子,如图所示:
在这里插入图片描述

索引为0的节点是 p-2,索引为1的节点是 p-3,以此类推。重新编号是为了让子序列seq与新索引值产生对应关系。

以上例来说,子序列seq的值为 [0,1],它的含义是:在新vnode中,重新编号后索引值为0和1的这两个节点在更新前后顺序没有变化

即重新编号后,索引值为0和1的节点不需要移动。对应到新vnode就是节点p-3索引为0,节点p-4索引为1,所以节点p-3和p-4所对应的真实DOM不需要移动。就是说只有节点 p-2和p-7可能需要移动。

为了完成节点的移动,还需要创建两个索引值 i和s:

  • i指向新vnode中的最后一个节点;
  • s指向最长递增子序列中的最后一个元素。
    在这里插入图片描述

接下来开启一个for循环,让变量i和s按照上图中箭头的方向移动,如下代码:

        if (moved) {
            // 计算最长递增子序列
            const seq = lis(source);
            let s = seq.length - 1;
            let i = count - 1;
            // for循环使得i递减,按照图中箭头的方向移动
            for (i; i >= 0; i--) {
                if (i !== seq[s]) {
                    // 符合条件则说明该节点需要移动
                } else {
                    // 当 i=== seq[s]时,说明该位置的节点不需要移动
                    s--;
                }
            }
        }

接下来就继续完善代码,执行更新。

  • 第一轮循环:初始时索引i指向节点 p-7,由于节点p-7对应的source数组中相同位置的元素索引为-1,所以将p-7作为全新节点进行挂载。
  • 第二轮循环:此时i=2,执行到下面代码中的第二步,i !== seq[s] 条件成立,节点p-2所对应的真实DOM需要移动。
  • 第三、四轮循环:p-4,p-3不需要移动。

此时的代码:

        if (moved) {
            // 计算最长递增子序列
            const seq = lis(source);
            let s = seq.length - 1;
            let i = count - 1;
            // for循环使得i递减,按照图中箭头的方向移动
            for (i; i >= 0; i--) {
                // 说明索引为i的节点是全新节点,将其挂载
                if (source[i] === -1) {
                    // 该节点在新vnode中的真实位置索引
                    const pos = i + newStart;
                    const newVnode = newChildren[pos];
                    // 该节点的下一个节点的位置索引
                    const nextPos = pos + 1;
                    // 锚点
                    const anchor = nextPos < newChildren.length
                        ? newChildren[nextPos].el
                        : null;
                    // 挂载
                    patch(null, newVnode, container, anchor);
                } else if (i !== seq[s]) {
                    // 该节点需要移动
                    const pos = i + newStart;
                    const newVnode = newChildren[pos];
                    const nextPos = pos + 1;
                    const anchor = nextPos < newChildren.length
                        ? newChildren[nextPos].el : null;
                    insert(newVnode.el, container, anchor);
                } else {
                    // 该节点不需要移动,只需要让s指向下一个位置
                    s--;
                }
            }
        }

在这里插入图片描述

完整代码:

// 快速diff算法
function patchKeyedChildren(n1, n2, container) {
    // 处理前置节点
    const oldChildren = n1.children;
    const newChildren = n2.children;
    let j = 0;
    let oldVnode = oldChildren[j];
    let newVnode = newChildren[j];
    // while循环向后遍历,直到遇到拥有不同key值的节点为止
    while (oldVnode.key === newVnode.key) {
        patch(oldVnode, newVnode, container);
        j++;
        oldVnode = oldChildren[j];
        newVnode = newChildren[j];
    }

    // 处理后置节点
    let oldEnd = oldChildren.length - 1;
    let newEnd = newChildren.length - 1;
    oldVnode = oldChildren[oldEnd];
    newVnode = newChildren[newEnd];
    // while循环从后向前遍历,直到遇到拥有不同key值的节点为止
    while (oldVnode.key === newVnode.key) {
        patch(oldVnode, newVnode, container);
        oldEnd--;
        newEnd--;
        oldVnode = oldChildren[oldEnd];
        newVnode = newChildren[newEnd];
    }

    // 预处理完毕后,如果满足如下条件,说明还有未被挂载的新增节点
    if (j > oldEnd && j <= newEnd) {
        // 锚点索引
        const anchorIndex = newEnd + 1;
        // 锚点元素
        const anchor = anchorIndex < newChildren.length
            ? newChildren[anchorIndex].el : null;
        // 采用while循环,逐个挂载新增节点
        while (j <= newEnd) {
            patch(null, newChildren[j++], container, anchor);
        }
    } else if (j > newEnd && j <= oldEnd) {
        // 预处理完毕后,如果满足如下条件,说明还有未被卸载的旧节点
        while (j <= oldEnd) {
            unmount(oldChildren[j++]);
        }
    } else {
        // 构造 source数组,保存新vnode中剩余未处理节点的数量
        const count = newEnd - j + 1;
        const source = new Array(count);
        source.fill(-1);

        // 起始索引
        const oldStart = j;
        const newStart = j;
        // 新增两个变量moved和pos
        let moved = false;
        let pos = 0;
        // 构建索引表
        const keyIndex = {};
        for (let i = newStart; i <= newEnd; i++) {
            keyIndex[newChildren[i].key] = i;
        }
        // 新增 patched变量,代表更新过的节点数量
        let patched = 0;
        for (let i = oldStart; i <= oldEnd; i++) {
            oldVnode = oldChildren[i];
            // 如果更新过的vnode数量 小于 需要更新的vnode数量,则执行更新
            if (patched <= count) {
                const k = keyIndex[oldVnode.key];
                if (typeof k !== 'undefined') {
                    newVnode = newChildren[k];
                    patch(oldVnode, newVnode, container);
                    source[k - newStart] = i;
                    // 判断节点是否需要移动
                    if (k < pos) {
                        moved = true;
                    } else {
                        pos = k;
                    }
                } else {
                    // 没找到
                    unmount(oldVnode);
                }
            } else {
                // 如果更新过的vnode数量 大于 需要更新的vnode数量,则卸载多余节点
                unmount(oldVnode);
            }
        }
        if (moved) {
            // 计算最长递增子序列
            const seq = lis(source);
            let s = seq.length - 1;
            let i = count - 1;
            // for循环使得i递减,按照图中箭头的方向移动
            for (i; i >= 0; i--) {
                // 说明索引为i的节点是全新节点,将其挂载
                if (source[i] === -1) {
                    // 该节点在新vnode中的真实位置索引
                    const pos = i + newStart;
                    const newVnode = newChildren[pos];
                    // 该节点的下一个节点的位置索引
                    const nextPos = pos + 1;
                    // 锚点
                    const anchor = nextPos < newChildren.length
                        ? newChildren[nextPos].el
                        : null;
                    // 挂载
                    patch(null, newVnode, container, anchor);
                } else if (i !== seq[s]) {
                    // 该节点需要移动
                    const pos = i + newStart;
                    const newVnode = newChildren[pos];
                    const nextPos = pos + 1;
                    const anchor = nextPos < newChildren.length
                        ? newChildren[nextPos].el : null;
                    insert(newVnode.el, container, anchor);
                } else {
                    // 该节点不需要移动,只需要让s指向下一个位置
                    s--;
                }
            }
        }
    }
}

4.总结:

快速diff算法实测中性能最优。它借鉴了文本diff中的预处理思路,先处理新旧vnode中相同的前置和后置节点。处理完后如果无法简单地通过挂载新节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引关系,构造出一个最长递增子序列,里面所指向的节点即为不需要移动的节点。

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

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

相关文章

宝塔部署前后端分离项目(Vue+SpringBoot)

目录 后端部分 配置Redis 前端部分 后端部分 1 先修改自己的speingboot配置文件&#xff0c;我的是yml文件 保证宝塔上建的数据库和自己代码里&#xff0c;就是配置文件中所建的数据库的名字是一致的密码也要保持一致&#xff0c;Redis也一样&#xff0c;如果有的话 2 记录…

关注电动汽车能效水平 提高续航能力

电动汽车&#xff08;EV&#xff09;近些年发展迅猛&#xff0c;已被汽车业内普遍认为是未来汽车发展的新方向&#xff0c;但是现如今电动汽车仍然存在一些短板&#xff0c;导致其还无法替代传统燃油车。对此&#xff0c;先想到的是电动车的续航问题。其实解决电动车续航问题主…

python 插值处理一维数据 interpolate

scipy库&#xff1a; 原码&#xff1a; https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html kind可选“linear”、“nearest”、“nearest-up”、“zero”、“slinear”、“quadratic”、“cubic”、“previous”或“next”之一。 “zero…

JSP sshOA办公系统myeclipse开发oracle数据库MVC模式java编程计算机网页设计

一、源码特点 JSP sshOA办公系统是一套完善的web设计系统&#xff08;系统采用ssh框架进行设计开发&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开 发。开发环境为TOMCAT7.0,Myecl…

音视频编解码流程与如何使用 FFMPEG 命令进行音视频处理

一、前言 FFMPEG 是特别强大的专门用于处理音视频的开源库。你既可以使用它的 API 对音视频进行处理&#xff0c;也可以使用它提供的工具&#xff0c;如 ffmpeg, ffplay, ffprobe&#xff0c;来编辑你的音视频文件。 本文将简要介绍一下 FFMPEG 库的基本目录结构及其功能&#…

OpenWrt 安装 WireGuard

下载 img镜像 https://downloads.openwrt.org/releases/22.03.2/targets/x86/generic/openwrt-22.03.2-x86-generic-generic-squashfs-combined-efi.img.gz解压 转化格式我的是Linux # 解压 gzip -d openwrt-22.03.2-x86-generic-generic-squashfs-combined-efi.img.gz #vmwa…

如何将数学曲线变为机器人轨迹-花式show爱心代码-turtlesim篇

第一步&#xff1a;找到曲线数学描述的网址。 阅读后了解曲线所对应的xy函数。 不要选太复杂的&#xff0c;毕竟先复现出来最重要的。 第二步&#xff0c;这个函数转为C代码。 //Lovegoal_x5.54.0*pow(sin(curve_t/200.0),3);goal_y5.5((13.0*cos(curve_t/200.0)-5.0*cos(curv…

网络工程师备考6章

6.1 OSI参考模型概述 计算机的整套理论是图灵提出来的,自此创办图灵奖(计算机类最高奖项)。科学远远比技术更重要。 OSI七层模型就是科学,就是理论,所以非常重要! 注:ISO是一个机构,OSI是一个协议:分别七层 6.2 OSI参考模型 注:在传输层中,什么是端到端,例如A,…

jsp+ssm计算机毕业设计ssm校园贫困补助系统【附源码】

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JSPSSM mybatis Maven等等组成&#xff0c;B/S模式 Mave…

使用vite构建vue3项目与官网构建区别

使用vite构建vue3项目 一、vue3官网文档的 https://www.javascriptc.com/vue3js/guide/installation.html#命令行工具-cli npm init vite-app cd npm installnpm run dev 二、vite官网文档的 https://cn.vitejs.dev/guide/#trying-vite-online 1 对应路径cmd 输入 npm create…

Grad-CAM简介-网络 热力图分析

论文名称&#xff1a;Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization 论文下载地址&#xff1a;https://arxiv.org/abs/1610.02391 推荐代码&#xff08;Pytorch&#xff09;&#xff1a;https://github.com/jacobgil/pytorch-grad-cam bi…

流媒体开源服务 MediaSoup 初识

目录 前言 正文 一、简单介绍 二、关键特色 1. 超强 SFU 功能 2. Node.js 模块 3. 客户端 SDK 三、架构组成 1. 关键实例 2. 重要模块 四、发展现状 前言 最近收看了一期微软&#xff08;中国&#xff09;关于云原生、大数据、AI 领域的开源服务创新的线上圆桌论坛&…

垃圾回收算法

1.种类 垃圾回收算法通常意义上有两种&#xff1a; 引用计数式垃圾收集追踪式垃圾收集 追踪式垃圾收齐又称“间接垃圾收集”&#xff0c;现在有很多算法大都遵循了分代收集理论&#xff0c;而分代收集理论的基础&#xff0c;却是以下俩个分代假说&#xff1a; 强分代假说&am…

MyPerf4J一个高性能、无侵入的Java性能监控和统计工具,有点东西!

背景 随着所在公司的发展&#xff0c;应用服务的规模不断扩大&#xff0c;原有的垂直应用架构已无法满足产品的发展&#xff0c;几十个工程师在一个项目里并行开发不同的功能&#xff0c;开发效率不断降低。 于是公司开始全面推进服务化进程&#xff0c;把团队内的大部分工程…

软件测试之移动app测试框架有哪些?

一、适用于Android的移动app测试框架 1.Espresso 十分流行的一款谷歌开发的Android测试框架&#xff0c;具备高性能性。可以创建非常简单直接的测试&#xff0c;而不必担心app的基础架构。此外&#xff0c;它是开源的&#xff0c;这使开发人员能够自定义框架。 2.Selendroid…

DOM算法系列003-获取节点A相对于节点B 的位置

UID: 20221214170009 aliases: tags: source: cssclass: created: 2022-12-14 1. 节点位置关系 两个节点A、B之间的位置关系总共有几种&#xff1f;我们第一时间能想到的&#xff1a; 节点A在节点B之后节点A在节点B之前节点A包含节点B节点A被节点B包含 除此之外&#xff0c;…

【python绘制地图——使用folium制作地图,可解决多数问题】

Python使用folium制作地图并生成png图片 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 第一章 使用folium制作地图 第二章 使用Html2Image生成png图片 第三章 使用reportlab制作pdf报告 提示&#xff1a;写完文章后&#xff0c;目录…

JavaWeb:Mysql(数据库管理系统)、Navicat(Mysql的图形化工具)

MyBatis是对JDBC的简化 以后的升级框架&#xff0c;基本都是围绕 JavaWeb程序 所做的升级 Mysql就是一个数据库管理系统&#xff0c;在系统里可以创建一个个数据库&#xff0c;即DBMS中创建一个个DB Mysqul官网https://downloads.mysql.com/archives/community/ 选择5.7.2…

PCB设计—AD20和立创EDA设计(1)创建项目

&#xff08;1&#xff09;纯新手建议先利用立创EDA画一个PCB&#xff0c;对PCB有一个简单的了解再学习AD20。 &#xff08;2&#xff09;立创EDA教程&#xff1a;立创EDA极速入门&#xff08;1&#xff09;——熟悉PCB和立创EDA基本操作&#xff1b;立创EDA极速入门&#xff0…

《纳瓦尔宝典》笔记二——停止出卖时间后,如何才能有收入

目录 一、引言 二、经典观点 1、没有捷径成功&#xff0c;所以不要抱走捷径心态 2、书的价值 3、一种杠杆-资产&#xff08;公司、股票、实业&#xff09;或被动收入&#xff08;媒体或代码&#xff09; 4、薪水与财富的区别 5、把自己产品化 6、共事的人和工作的内容比…