当前位置: 首页 > 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…...

王道考研编程题总结

我还在完善中&#xff0c;边复习边完善&#xff08;这个只是根据我自身总结的&#xff09; 一、 线性表 1. 结构体 #define MaxSize 40 typedef struct{ElemType data[MaxSize]&#xff1b;int length; }SqList 2. 编程题 1. 删除最小值 题意 &#xff1a;从顺序表中删除…...

手机租赁系统开发全攻略 创新服务助力企业智能转型

内容概要 在当今数字化飞速发展的时代&#xff0c;“手机租赁系统开发”正逐渐成为企业智能转型的必然选择。这一过程并不简单&#xff0c;但关键流程的解析将帮助企业理清思路。首先&#xff0c;了解需求和目标是基础&#xff0c;之后制定详细计划和流程图&#xff0c;让整件…...

git回退到某个版本git checkout和git reset命令的区别

文章目录 1. git checkout <commit>2. git reset --hard <commit>两者的区别总结推荐使用场景* 在使用 Git 回退到某个版本时&#xff0c; git checkout <commit> 和 git reset --hard <commit> 是两种常见的方式&#xff0c;但它们的用途和影响有很…...

如何使用Spring Boot进行Web开发?

Spring Boot 是一个基于 Java 的框架&#xff0c;它简化了新 Spring 应用的初始设置和开发过程。使用 Spring Boot 进行 Web 开发可以让你快速创建独立的、生产级别的基于 Spring 的应用。下面是使用 Spring Boot 进行 Web 开发的基本步骤&#xff1a; 文章目录 1. 环境准备2. …...

error=‘null‘], commandType=io.lettuce.core.RedisPublisher$SubscriptionCommand]

问题 查看java应用启动日志输出下面错误&#xff1a; errornull], commandTypeio.lettuce.core.RedisPublisher$SubscriptionCommand] Completing command LatencyMeteredCommand [typeINFO, outputStatusOutput [output# Server redis_version:4.0.14 redis_git_sha1:000…...

AI PC处理器ARM架构-引入NPU和大模型

AI PC处理器架构变化&#xff1a;ARM低功耗、引入NPU和大模型 AI进化加速端侧落地&#xff0c;新一轮浪潮蓄势待发(2024)”。ARM(Advanced RISC Machine)架构和x86架构是两种主要的处理器架构&#xff0c;它们在设计理念、应用场景和性能特点等方面有显著的差异。 ARM架构是一…...

python之opencv库Haar级联分类器检测人脸--‘haarcascade_frontalface_default.xml‘

python之opencv库Haar级联分类器检测人脸–‘haarcascade_frontalface_default.xml’ opencv库&#xff1a; 它由 Intel 公司发起并参与开发&#xff0c;其初衷是为了提供高效的计算机视觉算法实现。随着计算机视觉领域的发展&#xff0c;OpenCV不断更新和完善&#xff0c;吸引…...

「Mac畅玩鸿蒙与硬件37」UI互动应用篇14 - 随机颜色变化器

本篇将带你实现一个随机颜色变化器应用。用户点击“随机颜色”按钮后&#xff0c;界面背景会随机变化为淡色系颜色&#xff0c;同时显示当前的颜色代码&#xff0c;页面还会展示一只猫咪图片作为装饰&#xff0c;提升趣味性。 关键词 UI互动应用随机颜色生成状态管理用户交互…...

确定 POST 请求中的数据字段

在使用 requests 进行 HTTP 请求时&#xff0c;data 和 params 是两种常见的参数&#xff0c;用于传递不同类型的数据。以下是它们的作用和区别&#xff1a; 1. data 的作用 用于 POST 请求的主体。通常传递表单数据或 JSON 数据。在 HTTP 请求中&#xff0c;data 中的内容会…...

Linux - DNS服务器

六、DNS服务器 1、简介 DNS&#xff08;Domain Name System&#xff09;是互联网上的一项服务&#xff0c;它作为将域名和IP地址相互映射的一个分布式 数据库&#xff0c;能够使人更方便的访问互联网。 DNS系统使用的是网络的查询&#xff0c;那么自然需要有监听的port。DNS使…...

探究 SpringBoot 结合 MVC 高校办公室行政事务管理系统的设计与应用实现

摘 要 身处网络时代&#xff0c;随着网络系统体系发展的不断成熟和完善&#xff0c;人们的生活也随之发生了很大的变化&#xff0c;人们在追求较高物质生活的同时&#xff0c;也在想着如何使自身的精神内涵得到提升&#xff0c;而读书就是人们获得精神享受非常重要的途径。为了…...

蓝桥杯-扫雷

这题不难&#xff0c;就是麻烦一点&#xff0c;这里暴力求解了直接 题目链接&#xff1a; 扫雷 AC代码&#xff1a; import java.util.Scanner; // 1:无需package // 2: 类名必须Main, 不可修改public class Main {public static void main(String[] args) {Scanner scan ne…...

Hive高可用配置

在hive的商用上没有集群一说&#xff0c;而且它本身也不是数据库&#xff0c;只是hadoop的数据sql化工具&#xff0c;但是hive可以配置高可用&#xff0c;通常业内对元数据服务会开5个&#xff0c;而HS2服务开3个&#xff0c;来保证hive服务的高可用 配置方式也很简单&#xf…...

探索AI新世界!热门工具与学习资源免费获取

​抖知书老师推荐&#xff1a; 人工智能技术的迅速发展让人们既充满期待又有些迷茫。有人担忧被AI技术取代&#xff0c;有人却积极拥抱这场科技浪潮。无论你处于哪种心态&#xff0c;人工智能已经深入到我们生活的方方面面。如果你希望轻松掌握最新的AI工具与动态&#xff0c;…...

MAUI APP开发蓝牙协议的经验分享:与跳绳设备对接

在开发MAUI应用程序时&#xff0c;蓝牙协议的应用是一个重要的环节&#xff0c;尤其是在需要与外部设备如智能跳绳进行数据交换的场景中。以下是我在开发过程中的一些经验和心得&#xff0c;希望能为你的项目提供帮助。 1. 蓝牙协议基础 蓝牙协议是无线通信的一种标准&#x…...

常见Linux命令(详解)

文章目录 常见Linux命令文件目录类命令pwd 打印当前目录的绝对路径ls 列出目录内容cd 切换路径mkdir 建立目录rmdir 删除目录touch 创建空文件cp 复制文件或目录rm 移除文件或者目录mv 移动文件与目录或重命名cat 查看文件内容more 文件分屏查看器less 分屏显示文件内容head 显…...

LeetCode763. 划分字母区间(2024冬季每日一题 23)

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段&#xff0c;同一字母最多出现在一个片段中。 注意&#xff0c;划分结果需要满足&#xff1a;将所有划分结果按顺序连接&#xff0c;得到的字符串仍然是 s 。 返回一个表示每个字符串片段的长度的列表。 示例 1&a…...

【k8s 深入学习之 event 聚合】event count累记聚合(采用 Patch),Message 聚合形成聚合 event(采用Create)

参考 15.深入k8s:Event事件处理及其源码分析 - luozhiyun - 博客园event 模块总览 EventRecorder:是事件生成者,k8s组件通过调用它的方法来生成事件;EventBroadcaster:事件广播器,负责消费EventRecorder产生的事件,然后分发给broadcasterWatcher;broadcasterWatcher:用…...

Java--数组的定义与使用

1.数组的基本概念 1.1为什么用数组 在程序设计中,每一个数据总是对应一个变量.当数据量越大,就需要更多的变量来存储.我们将相同类型的数据存储到一个集合中,就可以更方便我们对数据进行访问,同时可以减少不断定义变量.这个集合就叫做数组 1.2数组的定义 数组是一种基本的数…...

tcpdump抓包wireshark分析

背景 分析特定协议的数据包&#xff0c;如 HTTP、DNS、TCP、UDP 等&#xff0c;诊断网络问题&#xff0c;例如连接故障、延迟和数据包丢失。 大概过程 1.安装tcpdump yum update yum install tcpdump2.抓包&#xff0c;从当前时间起&#xff0c;一小时后停止&#xff0c…...

qtcanpool 知 09:测试框架

文章目录 前言不满改进优化后语 前言 很久以前&#xff0c;作者写的代码都没有测试用例&#xff0c;最多就是写个 demo 验证一下&#xff0c;毕竟不是专业出身&#xff0c;也没经过大公司的洗礼。 后来&#xff0c;参与到一些项目才知道有专门的测试&#xff0c;而且开发也要测…...

使用Apache HttpClient发起一个GET HTTP请求

Apache HttpClient 是一个强大且灵活的Java库&#xff0c;用于处理HTTP请求。 它提供了广泛的功能&#xff0c;包括对不同HTTP方法的支持、连接管理、Cookie处理等。 无论是与RESTful API交互、下载网页内容还是自动化网页任务&#xff0c;Apache HttpClient 都能通过其简洁而…...

C++ STL 容器系列(三)list —— 编程世界的万能胶,数据结构中的百变精灵

STL系列学习参考&#xff1a; C STL系列__zwy的博客-CSDN博客https://blog.csdn.net/bite_zwy/category_12838593.html 学习C STL的三个境界&#xff0c;会用&#xff0c;明理&#xff0c;能扩展&#xff0c;STL中的所有容器都遵循这个规律&#xff0c;下面我们就按照这三个境…...

【前端学习笔记】TypeScript学习

1.什么是TypeScript TypeScript&#xff08;简称 TS&#xff09;是微软公司开发的一种基于 JavaScript &#xff08;简称 JS&#xff09;语言的编程语言。TypeScript 可以看成是 JavaScript 的超集&#xff08;superset&#xff09;&#xff0c;添加了类型系统和编译时类型检查…...

qt三大调试方法总结(printf\qDebug\qCDebug)

文章目录 1 传统方法2 qDebug传统方法扩展1 控制输出扩展2 日志格式扩展3 日志保存扩展4 源码定义护展5 开源扩展3 qCDebug方法扩展1 控制扩展2 格式化扩展3 保存日志扩展4 源码定义参考1 传统方法 #include<stdio.h> printf(“xboard hello printf”) 2 qDebug传统方法…...

耶鲁大学公开课《心理学导论》学习笔记:第 1 课 - 导论

概述 作为一个程序员&#xff0c;或者说&#xff0c;我们不管做什么行业&#xff0c;都可以或多或少的学习一些心理学 我们在生活工作中&#xff0c;其实都会有意无意的接触一些心理学原理&#xff0c;例如&#xff0c;【番茄工作法】、【内在动机与外在激励】 这里选择的是&…...

Android ConstraintLayout 约束布局的使用手册

目录 前言 一、ConstraintLayout基本介绍 二、ConstraintLayout使用步骤 1、引入库 2、基本使用&#xff0c;实现按钮居中。相对于父布局的约束。 3、A Button 居中展示&#xff0c;B Button展示在A Button正下方&#xff08;距离A 46dp&#xff09;。相对于兄弟控件的约束…...

STM32F030单片机AD采集应用总结

最近在设计一款产品的AD时&#xff0c;采集到的电压老是比输入电压0.2V左右&#xff0c;电路如图所示 查阅资料得知&#xff0c;STM32f030 的输入阻抗应小于 50K。于是将电阻改为 39K/10K&#xff0c;但情况依旧。随后&#xff0c;干脆将电阻值改为 3.9K/1K&#xff0c;虽有一定…...

Web开发基础学习——通过React示例学习模态对话框

Web开发基础学习系列文章目录 第一章 基础知识学习之通过React组件学习模态对话框 文章目录 Web开发基础学习系列文章目录前言一、创建新的 React 应用二、 创建模态对话框组件三、修改 App.js四、 添加样式五、启动应用六、访问应用总结 前言 模态对话框&#xff08;Modal D…...

实例分割详解

实例分割详解 引言 实例分割是计算机视觉领域的一项复杂任务&#xff0c;它要求模型能够识别图像中不同类别的对象&#xff0c;并对每个单独的对象进行像素级别的分类。与语义分割不同的是&#xff0c;实例分割不仅要区分不同的类别&#xff0c;还要识别同一类别中的不同个体…...

Flink四大基石之State(状态) 的使用详解

目录 一、有状态计算与无状态计算 &#xff08;一&#xff09;概念差异 &#xff08;二&#xff09;应用场景 二、有状态计算中的状态分类 &#xff08;一&#xff09;托管状态&#xff08;Managed State&#xff09;与原生状态&#xff08;Raw State&#xff09; 两者的…...

vue深入理解输入框字符限制的优化设计

文章目录 深入理解输入框字符限制的优化设计背景与挑战输入框限制的重要性常见需求 多种实现方法解析方法一&#xff1a;基于实时过滤的字符限制方法二&#xff1a;借助正则验证方法三&#xff1a;提交时二次校验 性能优化无障碍设计延伸场景与最佳实践1. 多语言国际化支持2. 动…...

MySQL的子查询

SQL语句中嵌套select语句,嵌套查询 案例&#xff1a; select * from t1 where column1 (select column1 from t2); 补&#xff1a; 1.子查询外部的语句可以是insert/update/delete/select的任何一个 2.位置也可以在where/from/select之后 类型&#xff1a; 1.标量子查询…...

Kubernetes架构原则和对象设计

云原生学习路线导航页&#xff08;持续更新中&#xff09; 快捷链接 Kubernetes常见问题解答 本文从 Google Borg系统的架构设计开始&#xff0c;深入讲解Kubernetes架构及组件的基本原理 1.什么是云计算 1.1.传统行业应用 假设有10台服务器&#xff0c;两个应用。小规模管…...

npm : 无法加载文件 D:\nodejs\npm.ps1,因为在此系统上禁止运行脚本

要以管理员身份打开PowerShell&#xff0c;请按照以下步骤操作&#xff1a; 在Windows搜索框中查找PowerShell&#xff1a; 在任务栏上&#xff0c;点击左下角的Windows徽标&#xff08;或按Win S键&#xff09;以打开搜索框。输入“PowerShell”以查找PowerShell应用程序。右…...

Linux CentOS

​阿里云开源镜像下载链接 https://mirrors.aliyun.com/centos/7/isos/x86_64/ VMware 安装 CentOS7 自定义 下一步 选择稍后安装操作系统 选择 输入 查看物理机CPU内核数量 CtrlShiftEsc 总数不超过物理机内核数量 推荐内存 自选 推荐 推荐 默认 拆分成多个 默认 自定义硬件…...

如何用注册机破解Reflexive游戏

相信有许多小朋友&#xff08;像我以前一样&#xff09;已经迫不及待地准备准备对浩瀚的、像三星堆一般的Reflexive游戏合集进行考古挖掘工作了。不巧的是&#xff0c;打开游戏之后发现常常提示要付费才能解锁完整版。 一、下载注册机与破解文件 首先&#xff0c;在我的永硕网…...

【算法day7】字符串:反转与替换

题目引用 反转字符串反转字符串II替换数字 1.反转字符串 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 示例 1&am…...

基于Linux C++多线程服务器 + Qt上位机开发 + STM32 + 8266WIFI的智慧无人超市

前言 针对传统超市购物车结账排队时间长、付款效率低的问题&#xff0c;提出了一种更符合现代社会人们购物方式-基于RFID的自助收银系统。习惯了快节奏生活的人们都会选择自助收银机结账&#xff0c;理由显而易见&#xff1a;自助收银机结账很方便&#xff0c;几乎不用排队&am…...

继电器测试的培训和学习资源有哪些推荐?

继电器是电气控制设备中常见的一种元件&#xff0c;用于实现电路的开关控制和保护功能。对于从事电气相关工作的人员来说&#xff0c;掌握继电器的测试技能是非常重要的。以下是一些推荐的继电器测试培训和学习资源&#xff1a; 1. 在线课程&#xff1a;许多在线学习平台提供了…...

学习日志020---qt信号与槽

作业 import sysfrom PySide6.QtWidgets import QApplication, QWidget,QPushButton,QLineEditfrom Form import Ui_Form from second import Ui_second from PySide6.QtCore import Qtclass MyWidget(QWidget,Ui_Form):def __init__(self):super().__init__()self.setupUi(se…...

小迪安全笔记 第四十四天 sql盲注 实战利用sql盲注 进行漏洞的利用

sql盲注的分类 什么是盲注 就是我们什么也不知道的情况下进行的注入 前边的注入 都是简单的注入 我们猜测 数据类型 之后 可以直接 union 去查 这种情况多用于 数据库增删查改中的 查 bool盲注也用于查 这个的情况的就是我们前边都试了 没有用 就需要…...

AMEYA360:上海永铭电子全新高压牛角型铝电解电容IDC3系列,助力AI服务器电源高效运转

随着数据中心和云计算的高速发展&#xff0c;AI服务器的能效要求日益提高。如何在有限空间内实现更高的功率密度和稳定的电源管理&#xff0c;成为AI服务器电源设计的一大挑战。永铭推出全新高压牛角型铝电解电容IDC3系列&#xff0c;以大容量、小尺寸的创新特性&#xff0c;为…...

SpringBoot Web 开发请求参数

SpringBoot Web 开发请求参数 简单的 web 请求: @RestController public class HelloController {@RequestMapping("sayHello")public String sayHello(){System.out.println("Hello World");return "hello world";} }获取请求参数 简单参数…...

力扣92.反转链表Ⅱ

题目描述 题目链接92. 反转链表 II 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], left …...

网络安全、Web安全、渗透测试之笔经面经总结(一)

本篇文章总结涉及以下几个方面&#xff1a; 一&#xff1a;对称加密非对称加密&#xff1f; 对称加密&#xff1a;加解密用同一密钥&#xff0c;密钥维护复杂n&#xff08;n-1&#xff09;/2&#xff0c;不适合互联网传输密钥&#xff0c;加解密效率高。应用于加密数据。 非…...

11 设计模式之代理模式(送资料案例)

一、什么是代理模式&#xff1f; 在现实生活中&#xff0c;我们常常遇到这样的场景&#xff1a;由于某些原因&#xff0c;我们可能无法亲自完成某个任务&#xff0c;便会委托他人代为执行。在设计模式中&#xff0c;代理模式 就是用来解决这种“委托”问题的&#xff0…...

matlab2024a安装

1.开始安装 2.点击安装 3.选择安装密钥 4.接受条款 5.安装密钥 21471-07182-41807-00726-32378-34241-61866-60308-44209-03650-51035-48216-24734-36781-57695-35731-64525-44540-57877-31100-06573-50736-60034-42697-39512-63953 6 7.选择许可证文件 8.找许可证文件 9.选…...

齐护机器人ModbusRTU RS485转TTL通信模块与ESP32 Arduino通信可Mixly的图形化编程Scratch图形化编程

齐护机器人ModbusRTU RS485-TTL通信模块 一、概念理解 Modbus协议是一种由Modicon公司&#xff08;现为施耐德电气Schneider Electric&#xff09;于1979年发表的网络通信协议&#xff0c;旨在实现可编辑逻辑控制器&#xff08;PLC&#xff09;之间的通信。 1.1 什么是Mod…...