当前位置: 首页 > news >正文

vue2 虚拟DOM 和 真实DOM (概念、作用、Diff 算法)

虚拟 DOM 和 真实DOM(概念、作用、Diff 算法)

1.1 概念
真实 DOM(Document Object Model):是浏览器中用于表示文档结构的树形结构。

<h2>你好</h2>

虚拟DOM:用 JavaScript 对象来模拟真实 DOM 的结构

{children: undefineddata: {}elm: h1key: undefinedsel: "h1"text: "你好h1"
}

步骤
1.用JS对象表示真实的DOM结构,生成一个虚拟DOM,再用虚拟DOM构建一个真实DOM树,渲染到页面
2.状态改变生成新的虚拟DOM,与旧的虚拟DOM进行比对,比对的过程就是DIFF算法,利用patch记录差异
3.把记录的差异用在第一个虚拟DOM生成的真实DOM上,视图就更新了。

(Vue.js 在早期开发过程中借鉴了 Snabbdom 的设计理念来构建自己的虚拟 DOM 系统)

1.2 作用

性能优化方面

真实DOM

  • 当直接操作真实 DOM 时,比如频繁地添加、删除或修改节点,会引起浏览器的重排(reflow)和重绘(repaint)。
  • 重排: DOM 结构的改变导致浏览器重新计算元素的几何属性,如位置、大小等;
  • 重绘:元素的外观发生改变,如颜色、背景等变化,只是重新绘制外观而不涉及布局调整。

虚拟DOM

  • 通过一种高效的 Diff 算法比较新旧虚拟 DOM 树的差异,可以快速地找出需要更新的部分,而不是每次都对整个 DOM 进行重新渲染。
  • 虚拟 DOM 的操作在 JavaScript 层面进行,比直接操作真实 DOM 快得多
  • 当组件的数据发生变化时,Vue.js 会收集一段时间内的数据变化,然后统一进行虚拟 DOM 的更新和差异比较,并根据差异更新真实 DOM,避免大量的无谓计算。

1.1 Diff 算法(源码地址)

它的主要作用是比较新数据与旧数据虚拟 DOM 树的差异,从而找出需要更新的部分,以便将这些最小化的变更应用到真实 DOM上,减少不必要的 DOM 操作,提高性能。

  • 树diff的时间复杂度O(n^3)
    第一,遍历tree1;第二,遍历tree2 第三,排序 1000个节点,要计算1亿次,算法不可用
  • 优化时间复杂度到O(n)
    只比较同一层级,不跨级比较
    tag不相同,直接删掉重建,不再深度比较
    tag和key,两者都相同,则认为是相同的节点,不再深度比较

在这里插入图片描述

  1. 首先sameVNode 比较一下新旧节点是不是同一个节点(同级对比,不跨级)

下图比较第二层级的右侧,左边是P,右边是div, 那么会认为这两个节点完全不同,直接删除旧的p替换新的div。
在这里插入图片描述
因为 dom 节点做跨层级移动的情况还是比较少的,一般情况下都是同一层级的 dom 的增删改。
但是 diff 算法除了考虑本身的时间复杂度之外,还要考虑一个因素:dom 操作的次数。
如果是一个list数组,新旧节点只是前后顺序的改变,直接删除新增,dom渲染成本会增加。

2.当节点类型相同的时候,Diff 算法会比较节点的属性是否有变化。如果属性有变化,就更新真实 DOM 节点的属性。

例如input节点,旧虚拟 DOM 中的value属性为abc,新虚拟 DOM 中的value属性为def,Diff 算法会更新真实 DOM 中input节点的value属性。

3.当节点类型,属性都相同,则比较是否存在子节点,
3.1子节点是文本节点

4.如果新节点和老节点都有子节点,需要进一步比较(双端diff核心updateChildren)

  • diff 算法我们从一端逐个处理的,叫做简单 diff 算法。简单 diff 算法其实性能不是最好的,比如旧的 vnode 数组是 ABCD,新的 vnode 数组是 DABC,按照简单 diff 算法,A、B、C 都需要移动。

那怎么优化这个算法呢?

  • vue使用的是双端 diff 算法:是头尾指针向中间移动,分别判断头尾节点是否可以复用,如果没有找到可复用的节点再去遍历查找对应节点的下标,然后移动。全部处理完之后也要对剩下的节点进行批量的新增和删除。

后来,Vue3 又对 diff 算法进行了一次升级,叫做快速 diff 算法(后续补充)。

双端diff算法updateChildren 逻辑

先贴代码,然后解释代码的含义(部分调用方法代码 放在最后面)

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {var oldStartIdx = 0; // 旧子节点start下标(游标)var newStartIdx = 0; // 新子节点start下标(游标)var oldEndIdx = oldCh.length - 1; // 旧子节点end下标(游标)var newEndIdx = newCh.length - 1; // 新子节点end下标(游标)var oldStartVnode = oldCh[0]; // 旧start节点(旧头)var newStartVnode = newCh[0]; // 新start节点(新头)var oldEndVnode = oldCh[oldEndIdx]; // 旧end节点(旧尾)var newEndVnode = newCh[newEndIdx]; // 新end节点(新尾)var oldKeyToIdx, idxInOld, vnodeToMove, refElm;var canMove = !removeOnly;checkDuplicateKeys(newCh);// 旧头子节点大于旧尾子节点,或者新头子节点大于新尾子节点时候跳出循环while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isUndef(oldStartVnode)) {// 旧头不存在,oldStartIdx++oldStartVnode = oldCh[++oldStartIdx]; } else if (isUndef(oldEndVnode)) {// 旧尾不存在,oldEndIdx--oldEndVnode = oldCh[--oldEndIdx];// sameVnode key相同,tag相同...都相同} else if (sameVnode(oldStartVnode, newStartVnode)) {// 旧头、新头相同,调用 patchVnode 进行更新节点(如果有子节点,继续调用updateChildren,diff查找子节点新旧dom不同),// 旧头右移一位oldStartVnode++、新头右移一位newStartVnode++patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);oldStartVnode = oldCh[++oldStartIdx];newStartVnode = newCh[++newStartIdx];} else if (sameVnode(oldEndVnode, newEndVnode)) {// 旧尾、新尾相同,patchVnode更新节点,旧尾左移一位oldEndIdx--、新尾左移一位newEndIdx--patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);oldEndVnode = oldCh[--oldEndIdx];newEndVnode = newCh[--newEndIdx];} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right// 旧头、新尾相同,patchVnode更新节点,并且将旧头移到oldCh最后,旧头右移一位 oldStartIdx++、新尾左移一位newEndIdx--patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));oldStartVnode = oldCh[++oldStartIdx];newEndVnode = newCh[--newEndIdx];} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left// 旧尾、新头相同,patchVnode更新节点,并且将旧尾移到oldCh最前面,新头右移一位 newStartIdx++、旧尾左移一位oldEndIdx--patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);oldEndVnode = oldCh[--oldEndIdx];newStartVnode = newCh[++newStartIdx];} else {// Undef 判断未定义if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }// isDef 判断新节点是否有key值唯一标示idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key] //去旧oldCh 里面找是否有一样的key: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);// 在oldCh列表中查找与newStartVnode匹配的节点索引,如果节点相同,返回索引if (isUndef(idxInOld)) {   // 遍历发现oldCh列表中没有和新节点相同的节点,创建新节点createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);} else { // 遍历发现oldCh列表中有和新节点key相同的节点vnodeToMove = oldCh[idxInOld];//进一步判断是否相等if (sameVnode(vnodeToMove, newStartVnode)) {// 相同,继续往下层判断更新节点patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);// 被移动的位置 undefined 占位 所以循环开头会增加isUndef 的判断oldCh[idxInOld] = undefined;// 将更改后的节点 移动位置canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);} else {// 节点不同,新建节点createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);}}newStartVnode = newCh[++newStartIdx];}}if (oldStartIdx > oldEndIdx) {// 旧子节点先遍历完,新子节点还有剩,调用addVnodes添加节点refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);} else if (newStartIdx > newEndIdx) {// 新子节点先遍历完,旧子节点还有剩,调用removeVnodes删除节点removeVnodes(oldCh, oldStartIdx, oldEndIdx);}
} 

开启一个循环,循环的条件就是 oldStart 不能大于oldEnd ,newStart不能大于newEnd,以下是循环的重要判断

  • 跳过undefined **if (isUndef(oldStartVnode))**

为什么会有undefined,老节点移动过程中,会产生undefined占位,之后的流程图会说明清楚。这里只要记住,如果旧开始节点为undefined,就后移一位;如果旧结束节点为undefined,就前移一位。

  • 快捷对比(https://www.jianshu.com/p/b9916979a740)**4个 else if(sameVnode(xxx))**

1 新开始和旧开始节点比对 如果匹配,表示它们位置都是对的,Dom不用改,就将新、旧节点开始的下标往后移一位即可。

2 旧结束和新结束节点比对 如果匹配,也表示它们位置是对的,Dom不用改,就将新、旧节点结束的下标前移一位即可。

3 旧开始和新结束节点比对 如果匹配,位置不对需要更新Dom视图,将旧开始节点对应的真实Dom插入到最后一位,旧开始节点下标后移一位,新结束节点下标前移一位。

4 旧结束和新开始节点比对 如果匹配,位置不对需要更新Dom视图,将旧结束节点对应的真实Dom插入到旧开始节点对应真实Dom的前面,旧结束节点下标前移一位,新开始节点下标后移一位。

  • key值查找(2.快捷对比都不满足的情况下) **}else {**

将旧节点数组剩余的vnode(oldStartIdx到oldEndIdx之间的节点)进行一次遍历,生成由vnode.key作为键,idx索引作为值的对象oldKeyToIdx,然后遍历新节点数组的剩余vnode(newStartIdx 到 newEndIdx 之间的节点),根据新的节点的key在oldKeyToIdx进行查找。

1 找到相同的key

  • 如果和已有key值匹配 那就说明是已有的节点,只是位置不对,则将找到的节点插入到 oldStartIdx 对应的 vnode 之前;并且,这里会将旧节点数组中 idxInOld 对应的元素设置为 undefined。
  • 如果和已有key值不匹配,那就说明是新的节点,那就创建一个对应的真实Dom节点,插入到旧开始节点对应的真实Dom前面即可

2 没有相同key

  • 没有找到对应的索引,则直接createElm创建新的dom节点并将新的vnode插入 oldStartIdx 对应的 vnode 之前。

以上是while内部处理,以下是while外部处理

  • 剩余元素处理(不满足循环条件后退出,循环外处理剩余元素)循环外
  • 旧节点数组遍历结束、新节点数组仍有剩余,经过两端对比查找都没有查找到,则说明新插入内容是处于 oldstartIdx与 oldEndIdx 之间的,所以可以直接在 newEndIdx 对应的 vnode 之前创建插入新节点即可。
  • 新节点数组遍历结束、旧节点数组仍有剩余,则遍历旧节点oldStartIdx 到 oldEndIdx 之间的剩余数据,进行移除
    因为旧节点oldStartIdx之前的数据和 oldEndIdx之后的数据都是对比确认之后的,且数量与新节点数组相同,则中间剩下的都是要删除的节点

以上便是vue2的diff的核心流程了,通过一个例子再来感受一下:

在每个循环单元中,我们执行下面的策略:

  • 分支0:旧头遇到空,指针向右移动 / 旧尾不存在,指针向左移动

  • 分支1:比较oldStart和newStart是否一致,如果一致,两个指针向右移动即可

  • 分支2:比较oldEnd和newEnd是否一致,如果一致,两个指针向左移动即可

  • 分支3:比较oldStart和newEnd是否一致,如果一致,就需要移动节点,移动节点都针对old的操作,将旧开始节点对应的真实Dom插入到oldEnd后面,oldStart节点下标后移一位,newEnd节点下标前移一位。

  • 分支4:比较newStart和oldEnd是否一致,如果一致,就需要移动节点,将oldEnd移动到oldStart的前一个。

  • 分支5:如果以上都没有命中,看看newStart是否在old中存在,如果存在,找到是第几个,假设是在old中的第i个位置,接下来将第i个位置的元素移动到oldStart的前一位,然后将当前第i位置空(所以会存在undef校验)。如果不存在说明创建了一个新的元素,需要执行创建策略。

在这里插入图片描述

循环1
第一步:A不等于B ,未命中分支1
第二步:D不等于C, 未命中分支2
第三步:A不等于C ,未命中分支3
第四步:B不等于D,未命中分支4
第五步:进入分支5,newStart在old中是否存在,执行之前的key值查找

第一轮循环结束:oldStart指向A,oldEnd指向D,newStart指向A,newEnd指向C
在这里插入图片描述

循环2
第一步:判断 A 等于 A,命中分支1,指针都向右移动。

第二轮循环结束:oldStart指向空,oldEnd指向D,newStart指向D,newEnd指向C
在这里插入图片描述

循环3: 第一步:oldStart遇到空,命中分支0,指针向右移动,oldStart指向C。

第3轮循环结束 oldStart指向C ,oldEnd指向D,newStart指向D,newEnd指向C

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f47f810ae9f644fb85865b48b5813203.png在这里插入图片描述

循环4
第一步:判断 D不等于C,未命中分支1
第二步:判断C不等于D,未命中分支2。
第三步:判断C等于C,命中分支3,将oldStart向oldEnd下一个移动,oldStart++。

第4轮循环结束:oldStart指向D,oldEnd指向D,newStart指向D,newEnd指向C。

在这里插入图片描述

循环5 第一步:判断 D等于D ,命中分支1,指针向右移动,oldStart++。

第5轮循环结束:oldStart指向C,oldEnd指向D,newStart指向C,newEnd指向C。
在这里插入图片描述
这时候循环已经结束,因为oldStart已经大于oldEnd。

当前的案例已经结束,old已经在相对次序上和new一模一样了,虽然在数据结构上有两个空在那里,而实际上的DOM结构已经移动到了正确的位置上,空对应在DOM上就是什么都没有,所以这个移动是正确的

如果 循环退出后,新节点数组仍有剩余 或者 旧节点数组仍有剩余 两者只有一种情况
接着以上的案例:

  • 旧节点数组遍历结束、新节点数组仍有剩余,经过两端对比查找都没有查找到,则说明新插入内容是处于 oldstartIdx与 oldEndIdx 之间的,所以可以直接在 newEndIdx 对应的 vnode 之前创建插入新节点即可。
  • 新节点数组遍历结束、旧节点数组仍有剩余,则遍历旧节点oldStartIdx 到 oldEndIdx 之间的剩余数据,进行移除
    因为旧节点oldStartIdx之前的数据和 oldEndIdx之后的数据都是对比确认之后的,且数量与新节点数组相同,则中间剩下的都是要删除的节点

部分调用函数代码简化版
isSameVnode简化版:

// asyncFactory 异步组件  isComment 注释节点
function sameVnode(a, b) {return (a.key === b.key &&a.asyncFactory === b.asyncFactory &&((a.tag === b.tag &&a.isComment === b.isComment &&isDef(a.data) === isDef(b.data) &&sameInputType(a, b)) ||(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))))
}

patchVnode简化版:将新节点的状态更新到旧节点对应的真实 DOM 节点上

function patchVnode(oldVnode, newVnode) {// 如果新旧节点是同一个对象,直接返回,无需更新if (oldVnode === newVnode) {return;}// 获取新旧节点对应的真实DOM元素const elm = oldVnode.elm;// 1. 更新属性部分const oldProps = oldVnode.data && oldVnode.data.attrs;const newProps = newVnode.data && newVnode.data.attrs;// 遍历新节点属性来更新或添加属性到真实DOM元素上for (let key in newProps) {//...}// 移除旧节点有但新节点没有的属性for (let key in oldProps) {//...}// 2. 更新子节点部分const oldChildren = oldVnode.children;const newChildren = newVnode.children;// 新旧子节点都是文本节点的情况,直接更新文本内容if (typeof oldChildren ==='string' && typeof newChildren ==='string') {if (oldChildren!== newChildren) {elm.textContent = newChildren;}} else if (Array.isArray(oldChildren) && Array.isArray(newChildren)) {// 继续updateChildren 处理ChildrenupdateChildren(elm, oldChildren, newChildren); }
}

findIdxInOld简化版:新节点在旧节点列表中查找是否有相同的

function findIdxInOld(newVnode, oldCh, start, end) {for (let i = start; i <= end; i++) {let oldVnode = oldCh[i];if (sameVnode(newVnode, oldVnode)) {// 如果节点相同(通过key和标签...判断),返回索引return i;}}return -1;
}

双端对比与生成索引 map 两种方式 减少了简单算法中的多次循环操作,新旧数组均只需要进行一次遍历即可将所有节点进行对比。

Snabbdom 待完善

本文参考文章
https://www.jb51.net/javascript/2968153yn.htm
https://www.zzsucai.com/biancheng/24014.html
https://news.sohu.com/a/564921201_121124376

相关文章:

vue2 虚拟DOM 和 真实DOM (概念、作用、Diff 算法)

虚拟 DOM 和 真实DOM&#xff08;概念、作用、Diff 算法&#xff09; 1.1 概念 真实 DOM&#xff08;Document Object Model&#xff09;&#xff1a;是浏览器中用于表示文档结构的树形结构。 <h2>你好</h2>虚拟DOM&#xff1a;用 JavaScript 对象来模拟真实 DOM…...

GEOBench-VLM:专为地理空间任务设计的视觉-语言模型基准测试数据集

2024-11-29 ,由穆罕默德本扎耶德人工智能大学等机构创建了GEOBench-VLM数据集&#xff0c;目的评估视觉-语言模型&#xff08;VLM&#xff09;在地理空间任务中的表现。该数据集的推出填补了现有基准测试在地理空间应用中的空白&#xff0c;提供了超过10,000个经过人工验证的指…...

说说Elasticsearch查询语句如何提升权重?

大家好&#xff0c;我是锋哥。今天分享关于【说说Elasticsearch查询语句如何提升权重&#xff1f;】面试题。希望对大家有帮助&#xff1b; 说说Elasticsearch查询语句如何提升权重&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 Elasticsearch 中&…...

2-2-18-9 QNX系统架构之文件系统(一)

阅读前言 本文以QNX系统官方的文档英文原版资料为参考&#xff0c;翻译和逐句校对后&#xff0c;对QNX操作系统的相关概念进行了深度整理&#xff0c;旨在帮助想要了解QNX的读者及开发者可以快速阅读&#xff0c;而不必查看晦涩难懂的英文原文&#xff0c;这些文章将会作为一个…...

Unity类银河战士恶魔城学习总结(P156 Audio Settings音频设置)

【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili 教程源地址&#xff1a;https://www.udemy.com/course/2d-rpg-alexdev/ 本章节实现了音频的大小设置与保存加载 音频管理器 UI_VolumeSlider.cs 定义了 UI_VolumeSlider 类&#xff0c;用于处理与音频设置相关的…...

springboot vue 会员收银系统 (12)购物车关联服务人员 订单计算提成 开源

前言 完整版演示 http://120.26.95.195/ 开发版演示 http://120.26.95.195:8889/ 在之前的开发进程中&#xff0c;我们完成订单的挂单和取单功能&#xff0c;今天我们完成购物车关联服务人员&#xff0c;用户计算门店服务人员的提成。 1.商品关联服务人员 服务人员可以选择 一…...

P3916 图的遍历(Tarjan缩点和反向建边)

P3916 图的遍历 - 洛谷 | 计算机科学教育新生态 写法一&#xff1a;Tarjan 思路&#xff1a;先运用Tarjan算法得到每个连通块中最大的编号&#xff0c;然后对每个连通块进行缩点重新建图&#xff0c;进行dfs&#xff0c;得到缩点后的连通块能够达到的最大编号。 Code: conste…...

Android13 允许桌面自动旋转

一&#xff09;需求-场景 Android13 实现允许桌面自动旋转 Android13 版本开始后&#xff0c;支持屏幕自动旋转&#xff0c;优化体验和兼容性&#xff0c;适配不同屏幕 主界面可自动旋转 二&#xff09;参考资料 android framework13-launcher3【06手机旋转问题】 Launcher默…...

cocotb value cocotb—基础语法对照篇

cocotb—基础语法对照篇 import cocotb from cocotb.triggers import Timer from adder_model import adder_model from cocotb.clock import Clock from cocotb.triggers import RisingEdge import randomcocotb.test() async def adder_basic_test(dut):"""Te…...

001-SpringBoot整合日志

SpringBoot整合日志 一、引入依赖二、配置 application.yml三、配置文件 logback.xml四、配置文件 WebConfigurerAdapter五、配置常量文件六、配置拦截器七、效果展示一、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId&…...

【Java基础面试题011】什么是Java中的自动装箱和拆箱?

相关知识补充&#xff1a;《Java从入门到精通(JDK17版)》_尚硅谷电子书.pdf Autism_Btkrsr/Blog_md_to_pdf - 码云 - 开源中国 (gitee.com) 回答重点 自动装箱&#xff1a;Java编译器自动将基本数据类型转换为包装类型 自动拆箱&#xff1a;Java编译器自动将包装类转换为基…...

ERROR in [eslint] Invalid Options ‘extensions‘ has been removed.

看着这个报错 感觉是版本不对引起的 ERROR in [eslint] Invalid Options: - Unknown options: extensions - extensions has been removed. ERROR in Error: Child compilation failed: [eslint] Invalid Options: - Unknown options: extensions - extensions has b…...

消息传递神经网络(Message Passing Neural Networks, MPNN)

消息传递神经网络&#xff08;Message Passing Neural Networks, MPNN&#xff09; 一、引言二、消息传递框架概述1.消息传递阶段&#xff08;1&#xff09;消息生成与传播-message&#xff08;2&#xff09;消息聚合-aggregate&#xff08;3&#xff09;消息更新-update&#…...

常用图像变换方法

伽马变换: void gamma_transform(cv::Mat &img, double gamma) {cv::Mat normalized;img.convertTo(normalized, CV_64F...

从被动响应到主动帮助,ProActive Agent开启人机交互新篇章

在人工智能领域&#xff0c;我们正见证着一场革命性的变革。传统的AI助手&#xff0c;如ChatGPT&#xff0c;需要明确的指令才能执行任务。但现在&#xff0c;清华大学联合面壁智能等团队提出了一种全新的主动式Agent交互范式——ProActive Agent&#xff0c;它能够主动观察环境…...

力扣hot100道【贪心算法后续解题方法心得】(三)

力扣hot100道【贪心算法后续解题方法心得】 十四、贪心算法关键解题思路1、买卖股票的最佳时机2、跳跃游戏3、跳跃游戏 | |4、划分字母区间 十五、动态规划什么是动态规划&#xff1f;关键解题思路和步骤1、打家劫舍2、01背包问题3、完全平方式4、零钱兑换5、单词拆分6、最长递…...

工业齐套管理虚拟现实仿真模拟软件

工业齐套管理虚拟现实仿真模拟软件是与法国最大的汽车制造商合作开发的一款虚拟现实仿真模拟软件&#xff0c;借助身临其境的虚拟现实环境&#xff0c;无需停止生产线&#xff0c;即可模拟仓库和提货区域。 工业齐套管理虚拟现实仿真模拟软件不仅适用于汽车工业&#xff0c;安全…...

ARP表、MAC表、路由表的区别和各自作用

文章目录 ARP表、MAC表、路由表的区别和各自作用同一网络内:ARP表request - 请求reply - 响应 MAC地址在同一网络内,交换机如何工作? 不同网络路由表不同网络通信流程PC1到路由器路由器到PC2流程图 简短总结 ARP表、MAC表、路由表的区别和各自作用 拓扑图如下: 同一网络内:…...

Android 使用OpenGLES + MediaPlayer 获取视频截图

概述 Android 获取视频缩略图的方法通常有: ContentResolver: 使用系统数据库MediaMetadataRetriever: 这个是android提供的类&#xff0c;用来获取本地和网络media相关文件的信息ThumbnailUtils: 是在android2.2&#xff08;api8&#xff09;之后新增的一个&#xff0c;该类为…...

浏览器的事件循环机制

浏览器和Node的事件循环机制 引言浏览器的事件循环机制 引言 由于JS是单线程的脚本语言&#xff0c;所以在同一时间只能做一件事情&#xff0c;当遇到多个任务时&#xff0c;我们不可能一直等待任务完成&#xff0c;这会造成巨大的资源浪费。为了协调时间&#xff0c;用户交互…...

css实现圆环展示百分比,根据值动态展示所占比例

代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

MongoDB学习和应用(高效的非关系型数据库)

一丶 MongoDB简介 对于社交类软件的功能&#xff0c;我们需要对它的功能特点进行分析&#xff1a; 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具&#xff1a; mysql&#xff1a;关系型数据库&am…...

无法与IP建立连接,未能下载VSCode服务器

如题&#xff0c;在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈&#xff0c;发现是VSCode版本自动更新惹的祸&#xff01;&#xff01;&#xff01; 在VSCode的帮助->关于这里发现前几天VSCode自动更新了&#xff0c;我的版本号变成了1.100.3 才导致了远程连接出…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

基于数字孪生的水厂可视化平台建设:架构与实践

分享大纲&#xff1a; 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年&#xff0c;数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段&#xff0c;基于数字孪生的水厂可视化平台的…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案

随着新能源汽车的快速普及&#xff0c;充电桩作为核心配套设施&#xff0c;其安全性与可靠性备受关注。然而&#xff0c;在高温、高负荷运行环境下&#xff0c;充电桩的散热问题与消防安全隐患日益凸显&#xff0c;成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...

【Linux】Linux 系统默认的目录及作用说明

博主介绍&#xff1a;✌全网粉丝23W&#xff0c;CSDN博客专家、Java领域优质创作者&#xff0c;掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围&#xff1a;SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill

视觉语言模型&#xff08;Vision-Language Models, VLMs&#xff09;&#xff0c;为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展&#xff0c;机器人仍难以胜任复杂的长时程任务&#xff08;如家具装配&#xff09;&#xff0c;主要受限于人…...