vue2 diff算法及虚拟DOM
概括:diff算法,虚拟DOM中采用的算法,把树形结构按照层级分解,只比较同级元素,不同层级的节点只有创建和删除操作。
一、虚拟DOM
(1) 什么是虚拟DOM?
虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。这个概念是由 React 率先开拓,随后在许多不同的框架中都有不同的实现,当然也包括 Vue。
虚拟DOM本质上就是用来描述真实DOM的JavaScript对象。
举例:
<div id="hello">我是奥特曼</div>
编译成虚拟DOM
const vnode = {type: 'div',props: {id: 'hello'},children: [/* 更多 vnode */],text:'我是奥特曼'
}
(2)为什么要有虚拟DOM
1.能够减少操作真实DOM,对于浏览器而言有很大的性能提升,对于开发而言有很大的工作效率(同时避免了代码繁琐)。
2.想要修改数据 直接修改js对象即可,当然我们基于框架去开发也不用关心数据如何去更新视图。
3.例如新节点中,只有一部分节点进行变动时,只需要更新要变更的一部分,另一部分完全可以使用之前的节点。也避免了全部更新渲染。
最终目的:新虚拟DOM和旧虚拟DOM 进行diff计算,(精细化比较),算出应该如何最小量更新,最后反应到真正的DOM上。
二、snabbdom
在vue2中diff算法借鉴了snabbdom库,这个库也就包含着如何生成虚拟DOM和diff算法。
方法包括:
h( ) 函数用来生成虚拟DOM
vnode( ) 函数 通过h调用vnode 达到返回虚拟DOM的格式
patch( ) 函数用来比较新旧节点是否是一个节点,如果不是进行暴力更新,如果是进行对比
patchVnode( ) 函数用来详细对比 当遇见新旧节点都是数组时调用updataChildren
updataChildren( ) 函数用来对比新旧节点都是数组的情况
createElement( ) 讲虚拟dom创建出真实dom
以下函数均为简单版实现核心 并为源码
(1)h( )函数
h函数的使用方式:
import { h } from 'vue'// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])
当然传参的方式有很多,最普通的写法
h(a,{},'文字')
当然第二个就可以去表示是参数的一些属性,当没有属性的时候你也可以不传,或者第二个参数可以传子集元素的集合等, snabbdom就是在这方面做了很多个判断。
h(a,'文字')
实现一个简单的h函数
当然如果你想了解的更透彻可以 clone snabbdom库的代码进行查看。
git clone https://github.com/snabbdom/snabbdom.git
由于snabbdom传参形式较多, 这里就实现一个简单的h函数,只实现三种场景
① h('div', {}, '文字')② h('div', {}, [h()])③ h('div', {}, h())
vnode( ) 函数
export default function vnode(sel, data, children, text, elm) {const key = data == undefined ? undefined : data.key;return { sel, data, children, text, elm, key };
}
vnode就是把传进来的参数以对象的形式返回出去,返回出一个vnode的形式
sel: 元素/标签
data:属性
children:子元素vnode的集合
text:文字
elm:节点
h( )函数
import vnode from "./vnode";
/*** 产生虚拟DOM树 返回的是一个对象* 低配版本的h函数,这个函数必须接受三个参数,缺一不可* @param {*} sel* @param {*} data* @param {*} c* 调用只有三种形态* ① h('div', {}, '文字')* ② h('div', {}, [h()])* ③ h('div', {}, h())*/
export default function (sel, data, c) {// 检查参数个数if (arguments.length !== 3) {throw new Error("请传入只三个参数!");}// 检查参数c的类型if (typeof c === "string" || typeof c === "number") {// 说明现在是 ① h('div', {}, '文字')return vnode(sel, data, undefined, c, undefined);} else if (Array.isArray(c)) {// 说明是 ② h('div', {}, [])let children = [];// 遍历 数组 cfor (let item of c) {// 如果是数组传入的项一定有sel属性 也就必须是h函数的格式 不考虑数组中有包含文字的情况if (!(typeof item === "object" && item.hasOwnProperty("sel"))) {throw new Error("传入的数组有不是h函数的项");}// 不用执行c[i], 调用的时候执行了,只要收集children.push(item);}return vnode(sel, data, children, undefined, undefined);} else if (typeof c === "object" && c.hasOwnProperty("sel")) {// 说明是 ③ h('div', {}, h())let children = [c];return vnode(sel, data, children, undefined, undefined);} else {throw new Error("传入的参数类型不对!");}
}
(2)createElement( )函数
把虚拟节点从(vnode)变成真实DOM元素
/*** 创建节点。将vnode虚拟节点创建为DOM节点* 是孤儿节点,不进行插入操作* @param {object} vnode * @returns {object} domNode 返回DOM节点*/
export default function createElement(vnode) {// 创建一个DOM节点,这个节点现在是孤儿节点,最后返回这个DOM节点let domNode = document.createElement(vnode.sel);// 判断是有子节点还是有文本if (vnode.text !== "" &&(vnode.children === undefined || vnode.children.length === 0)) {// 说明没有子节点,内部是文本domNode.innerText = vnode.text;} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {// 说明内部是子节点,需要递归创建节点 // 遍历vnode.children数组for (let ch of vnode.children) {// 递归调用 创建出它的DOM,一旦调用createElement意味着创建出DOM了。并且它的elm属性指向了创建出的dom,但是没有上树,是一个孤儿节点let chDOM = createElement(ch);// console.log(ch);// 上树domNode.appendChild(chDOM);}}// 补充elm属性vnode.elm = domNode;// 返回domNode DOM对象return domNode;
}
(3)patch( )函数
patch函数基本上也是diff算法的核心,比较这新虚拟节点和旧虚拟节点的不同。
export default function patch(oldVnode, newVnode) {// 判断传入的第一个参数是 DOM节点 还是 虚拟节点if (oldVnode.sel == "" || oldVnode.sel === undefined) {// 说明是DOM节点,此时要包装成虚拟节点oldVnode = vnode(oldVnode.tagName.toLowerCase(), // sel{}, // data[], // childrenundefined, // textoldVnode // elm);}// 此时新旧节点都是虚拟节点了// 判断 oldVnode 和 newVnode 是不是同一个节点if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {console.log("是同一个节点,需要精细化比较");patchVnode(oldVnode, newVnode);} else {console.log("不是同一个节点,暴力插入新节点,删除旧节点");// 创建 新虚拟节点 为 DOM节点let newVnodeElm = createElement(newVnode);// 获取旧虚拟节点真正的DOM节点let oldVnodeElm = oldVnode.elm;// 判断newVnodeElm是存在的if (newVnodeElm) {// 插入 新节点 到 旧节点 之前oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm);}// 删除旧节点oldVnodeElm.parentNode.removeChild(oldVnodeElm);}
}
注释写的比较详细,大致三部分:
1. 第一次有可能传入进来的是一个元素,因为第一次还没有节点,需要把元素包装成虚拟节点 vnode
2. 如果 旧虚拟节点和新虚拟节点key和元素标签一样时 可以进入深层比较 调用patchVnode函数
3. 否则把新节点创建成dom元素 插入到旧节点之前 并移除旧节点
(4)patchVnode( )函数
import updateChild from "./updateChildren";
import createElement from "./createElement";
export default function patchVnode(oldVnode, newVnode) {// 第一种情况 全都一样 不需要操作if (oldVnode == newVnode) return;// 第二种情况新节点是文字if ( newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0) ) {// 如果旧节点文字不等于新节点文字 直接替换if (oldVnode.text != newVnode.text) {oldVnode.elm.innerText = newVnode.text;}// 第三种情况 新节点是数组} else {// 如果新旧节点都是数组 也是最复杂的情况if (oldVnode.children && oldVnode.children.length > 0) {updateChild(oldVnode.elm, oldVnode.children, newVnode.children);// 如果旧节点是文字 新节点是数组} else {// 清空旧节点文字 把子节点给旧节点的elm元素上oldVnode.elm.innerText = "";for (let i = 0; i < newVnode.children; i++) {const childDom = newVnode.children[i];childDom.elm = createElement(childDom);oldVnode.elm.appendChild(childDom.elm);}}}newVnode.elm = oldVnode.elm;
}
(5)updateChild( )函数
这也是函数中最复杂的部分了,如果新旧节点均为数组,我们要做最细致的比较。
例如:旧节点 A,B,C 新节点 A,C,B 我们是肯定希望去移动它,并不是删除B,C重新创建出C,B
例如: 旧节点 A,B,C 新节点 D,A,B,C,E 同样我们需要在A前面创建出D 在C后面创建出E
当然复杂的场景还有很多很多,这也是我们都要考虑的,这时候就需要用到snabbdom中的四个指针概念了。
那什么是新前旧前、新后旧后呢?
新前就是新节点(newVnode)中第一个,旧前就是旧节点(oldVnode)中的第一个, 新后旧后同理。
遵循四种查找方式
1. 例如 第一种查询方式 新前和旧前(A==B)匹配上了 newStartIdx 和 oldStartIdx 都进行 +1 这时候新前旧前指针进行移动,移动后重新执行四种命中方式, 直到四种命中方式都找不到为止。
2. 若四种命中方式都查不着不到,则需要在oldVnode中进行循环查找,找到则插入到oldStartIdx之前,若循环也找不到则 创建出新元素 插入到oldEenIdx之前。
3. 若新节点中有剩余 代表要新增元素 把新前和新后中间的元素 创建出真实DOM 插入到新后位置的之前。
4.若旧节点中有剩余 代表要删除元素 把旧前和旧后中间的元素 进行删除。
注意:当命中③时 需要移动节点 将当前新指向节点移动到旧节点之后,当命中④时,需要移动节点 将新指向节点移到到旧节点之前。
下面来举一个全面的 场景 以此用到所有命中方便理解
当然 图中最后一步是找不到的场景,当然也有找到的场景,在代码中 在 keyMap 中进行查找
例如 keyMap = { A: 0,B:1,C:2 } 若匹配到了需要调用patchVnode函数 并且 需要移到指定的位置上,若匹配不上则插入到旧前节点之前。
export default function updataChild(parentElm, oldCh, newCh) {// 旧前let oldStartIdx = 0;// 新前let newStartIdx = 0;// 新后let newEndIdx = newCh.length - 1;// 旧后let oldEndIdx = oldCh.length - 1;// 旧前节点let oldStartVnode = oldCh[0];// 旧后节点let oldEndVnode = oldCh[oldEndIdx];// 新前节点let newStartVnode = newCh[0];// 新后节点let newEndVnode = newCh[newEndIdx];let keyMap = null;// 进入循环while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// debugger// 新前 旧前// 1. 如果相同在// 首先应该不是判断四种命中,而是略过已经加了undefined标记的项if (oldStartVnode === null || oldCh[oldStartIdx] === undefined) {oldStartVnode = oldCh[++oldStartIdx];} else if (oldEndVnode === null || oldCh[oldEndIdx] === undefined) {oldEndVnode = oldCh[--oldEndIdx];} else if (newStartVnode === null || newCh[newStartIdx] === undefined) {newStartVnode = newCh[++newStartIdx];} else if (newEndVnode === null || newCh[newEndIdx] === undefined) {newEndVnode = newCh[--newEndIdx];} else if (checkSameVnode(oldStartVnode, newStartVnode)) {// 新前与旧前console.log(" ①1 新前与旧前 命中");// 精细化比较两个节点 oldStartVnode现在和newStartVnode一样了patchVnode(oldStartVnode, newStartVnode);// 移动指针,改变指针指向的节点,这表示这两个节点都处理(比较)完了oldStartVnode = oldCh[++oldStartIdx];newStartVnode = newCh[++newStartIdx];} else if (checkSameVnode(oldEndVnode, newEndVnode)) {// 新后与旧后console.log(" ②2 新后与旧后 命中");patchVnode(oldEndVnode, newEndVnode);oldEndVnode = oldCh[--oldEndIdx];newEndVnode = newCh[--newEndIdx];} else if (checkSameVnode(oldStartVnode, newEndVnode)) {// 新后与旧前console.log(" ③3 新后与旧前 命中");patchVnode(oldStartVnode, newEndVnode);// 当③新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的 旧后的后面// 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);oldStartVnode = oldCh[++oldStartIdx];newEndVnode = newCh[--newEndIdx];} else if (checkSameVnode(oldEndVnode, newStartVnode)) {// 新前与旧后console.log(" ④4 新前与旧后 命中");patchVnode(oldEndVnode, newStartVnode);// 当④新前与旧后命中的时候,此时要移动节点。移动 新前(旧后) 指向的这个节点到老节点的 旧前的前面// 移动节点:只要插入一个已经在DOM树上的节点,就会被移动parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);oldEndVnode = oldCh[--oldEndIdx];newStartVnode = newCh[++newStartIdx];} else {// 四种都没有匹配到,都没有命中console.log("四种都没有命中");// 寻找 keyMap 一个映射对象, 就不用每次都遍历old对象了if (!keyMap) {keyMap = {};// 记录oldVnode中的节点出现的key// 从oldStartIdx开始到oldEndIdx结束,创建keyMapfor (let i = oldStartIdx; i <= oldEndIdx; i++) {const key = oldCh[i].key;if (key !== undefined) {keyMap[key] = i;}}}console.log(keyMap);// 寻找当前项(newStartIdx)在keyMap中映射的序号const idxInOld = keyMap[newStartVnode.key];if (idxInOld === undefined) {// 如果 idxInOld 是 undefined 说明是全新的项,要插入// 被加入的项(就是newStartVnode这项)现不是真正的DOM节点parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);} else {// 说明不是全新的项,要移动const elmToMove = oldCh[idxInOld];patchVnode(elmToMove, newStartVnode);// 把这项设置为undefined,表示我已经处理完这项了oldCh[idxInOld] = undefined;// 移动,调用insertBefore也可以实现移动。parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);}// newStartIdx++;newStartVnode = newCh[++newStartIdx];}}// 如果新节点还有剩余 要新增if (newStartIdx <= newEndIdx) {console.log('新节点有剩余',newCh[newEndIdx+1]);let before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;for (let i = newStartIdx; i <= newEndIdx; i++) {parentElm.insertBefore(createElement(newCh[i]), before);// parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);}}else if (oldStartIdx <= oldEndIdx) {console.log("旧节点有剩余");for (let i = oldStartIdx; i <= oldEndIdx; i++) {parentElm.removeChild(oldCh[i].elm);}}
}function checkSameVnode(oldStartVnode, newStartVnode) {return (oldStartVnode.key == newStartVnode.key &&oldStartVnode.sel == newStartVnode.sel);
}
相关文章:
vue2 diff算法及虚拟DOM
概括:diff算法,虚拟DOM中采用的算法,把树形结构按照层级分解,只比较同级元素,不同层级的节点只有创建和删除操作。 一、虚拟DOM (1) 什么是虚拟DOM? 虚拟 DOM (Virtual DOM,简称 VDOM) 是一种…...
Ray和极客们的创新之作,2月18日来发现
所在论坛:数据库技术创新&云原生论坛分享时段:2.18 10:30-11:00分享主题:云原生数据库PieCloudDB :Unbreakable安全特性剖析分享嘉宾:王淏舟,拓数派资深研发工程师 由中国开源软件推进联盟PostgreSQL分…...
Dubbo 源码分析 – 集群容错之 Router
1. 简介 上一篇文章分析了集群容错的第一部分 – 服务目录 Directory。服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由。上一篇文章关于服务路由相关逻辑没有细致分析,一笔带过了,本篇文章将对此进行详细的分析。首先&…...
行人检测(人体检测)3:Android实现人体检测(含源码,可实时人体检测)
行人检测(人体检测)3:Android实现人体检测(含源码,可实时人体检测) 目录 行人检测(人体检测)3:Android实现人体检测(含源码,可实时人体检测) 1. 前言 2. 人体检测数据集说明 3. 基于YOLOv5的人体检测模型训练 4.人体检测模型…...
【图像分类】基于PyTorch搭建LSTM实现MNIST手写数字体识别(单向LSTM,附完整代码和数据集)
写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 提起LSTM大家第一反应是在NLP的数据集上比较常见,不过在图片分类中,它同样也可以使用。我们以比较熟悉的 mnist…...
Kotlin 1.8.0 现已发布,有那些新特性?
文章目录**如何安装 Kotlin 1.8.0****如果您遇到任何问题****更多文章和视频**结语Kotlin 1.8.0 版本现已发布,以下是其部分最大亮点: JVM 的新实验性功能:递归复制或删除目录内容提升了 kotlin-reflect 性能新的-Xdebug编译器选项ÿ…...
likeshop单商户SaaS商城系统—无限多开,搭建多个商城
likeshop单商户SaaS商城系统:适用于多开(SaaS)、B2C、单商户、自营商城场景,完美契合私域流量变现闭环交易使用,系统拥有丰富的营销玩法,强大的分销能力,支持DIY多模板,前后端分离。…...
Bean(Spring)的执行流程和生命周期
Bean(Spring)的执行流程具体的流程就和我们创建Spring基本相似。启动 Spring 容器 -> 实例化 Bean(分配内存空间,从无到有) -> Bean 注册到 Spring 中(存操作) -> 将 Bean 装配到需要的…...
工作记录------PostMan自测文件导入、导出功能
工作记录------PostMan自测文件导入、导出功能 测试文件导出 背景:写了一个文件下载功能,是数据写到excel中,下载,使用PostMan点击send后,返回报文是乱码。 解决办法: 点击send下面的 send and Downlo…...
上海亚商投顾:沪指震荡上行 大消费板块全线走强
上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。市场情绪三大指数今日震荡反弹,沪指全天低开高走,深成指、创业板指均涨超1%。工程机械板块集体大涨&a…...
linux中的图形化UDP调试工具
sokit freeware version: 1.3.1 (GPLv3) website: https://github.com/sinpolib/sokit/ 这是一个TCP / UDP数据包收发和传输工具 linux汉化 默认是英文版本的,如果想使用中文,把软件目录下的sokit.lan_rename重命令为sokit.lan再次打开软件就发现已经…...
前端react面试题指南
概述下 React 中的事件处理逻辑 抹平浏览器差异,实现更好的跨平台。避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁。方便事件统一管理和事务机制。 为了解决跨浏览器兼容性问题࿰…...
深入浅出原核基因表达调控(乳糖操纵子、色氨酸操纵子)
原核基因表达调控 前言 自然界里,能量时有时无,各种生命为了让自己能够活下去,需要适应环境,在不同的环境合成不同的蛋白质。 原核生物体内有很多细胞,细胞里面有很多蛋白质,但是这些蛋白质在这些细胞里…...
10分钟理解Mysql索引
一、索引介绍 索引是什么 官方介绍索引是帮助MySQL高效获取数据的数据结构。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度。 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往是存储在磁盘…...
nVisual综合布线可视化管理系统解决方案
一、综合布线管理系统的必要性 如今企事业单位办公人员变化很快,如果还是采用传统方式通过工程竣工图或者网络拓扑图来进行网络维护工作会非常麻烦,并且对管理人员的要求也会很高,管理人员需要清楚的知道工作区的信息点与配线架点之间的对…...
34岁测试工程师被辞退,难道测试岗位真的只是青春饭吗?
一:前言:人生的十字路口静坐反思 入软件测试这一行至今已经10年多,承蒙领导们的照顾与重用,同事的支持与信任,我的职业发展算是相对较好,从入行到各类测试技术岗位,再到测试总监,再…...
Java中常见的空指针异常
参考链接: java中什么是空指针异常以及为什么会产生空指针异常天上的云川的博客-CSDN博客什么是java空指针 java中容易产生空指针异常:NullPointerException的场景火龙映天的博客-CSDN博客java怎么制造空指针异常 java空指针异常是什么、怎么发生、如何…...
d亚当替换工厂模式
对象工厂替代方案 一般,需要无需用模块构造器触发d运行时的挑剔循环检测的方法来注册工厂.很多时候,混合模块构造器正是想要方法,但它有全局全开或全闭的循环检测算法. 要全局关闭它,请在Main文件中,添加以下代码行: extern(C) __gshared string[] rt_options ["oncycl…...
Real-time Scene Text Detection with Differentiable Binarization
Abstract 近年来,基于分割的方法在文本检测场景中非常流行,因为分割结果可以更准确地描述曲线文本等各种形状的场景文本。然而,二值化的后处理对于分割检测是必不可少的,它将分割方法产生的概率图转换为文本框/区域。本文提出了一…...
国外客户只想跟工厂合作?可以这样破解
1.客户是愿意和外贸公司合作还是更愿意和工厂合作?一个外贸公司的朋友说:“我去工厂接待过七八次外国人,基本上都是英国、德国、日本、加拿大、美国的。”贸易公司根本不避讳自己是贸易公司,外国人也不在乎。他们更关心的是贸易公司能否妥善安…...
c++重中之重:“换个龟壳继续套娃“:运算符重载等的学习
文章目录 前言一.运算符重载二.const成员三.取地址重载总结前言 上一期我们讲到类的6个默认构造函数中的拷贝构造函数,这一期我们继续往下讲,当然难点肯定是运算符重载了。 一、运算符重载 运算符重载是c为了增强代码的可读性引入了运算符重载…...
RabbitMQ简单使用
这篇文章通过一个最简单的例子,让初学者能了解RabbitMQ如何完成生产消息和消息的。 所有的程序员在学习一门新技术的时候,都是从 Hello World 进入到Colorful World的,本节也将按照惯例,从HelloWorld开始,演示RabbitMQ…...
Lambda表达式
👌 棒棒有言:也许我一直照着别人的方向飞,可是这次,我想要用我的方式飞翔一次!人生,既要淡,又要有味。凡事不必太在意,一切随缘,缘深多聚聚,缘浅随它去。凡事…...
JSON数据格式【学习记录】
JSON介绍 JSON(JavaScript Objet Notation)是一种轻量级的数据交换格式。它使得人们很容易的进行阅读和编写。同时也方便了机器进行解析和生成。它采用一种键:值对的文本格式来存储和表示数据,在系统交换数据过程中常常被使用,是…...
LeetCode——1234. 替换子串得到平衡字符串
一、题目 有一个只含有 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符,且长度为 n 的字符串。 假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。 给你一个这样的字符串 s,请通过「替换一个子串」的方式&a…...
Web自动化测试——selenium篇(二)
文章目录一、浏览器相关操作二、键盘操作三、鼠标操作四、弹窗操作五、下拉框选择六、文件上传七、错误截图一、浏览器相关操作 浏览器窗口大小设置 driver.manage().window().maximize();//窗口最大化 driver.manage().window().minimize();//窗口最小化 driver.manage().wi…...
RK3399平台开发系列讲解(文件系统篇)虚拟文件系统的数据结构
🚀返回专栏总目录 文章目录 一、超级块二、挂载描述符三、文件系统类型四、索引节点五、目录项沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍虚拟文件系统的数据结构。 一、超级块 文件系统的第一块是超级块,用来描述文件系统的总体信息。当我们把文件系…...
企业财务管理升级,智慧税务和数据可视化打造新标准
一、引言在发展社会主义市场经济的过程中,税收承担着组织财政收入、调控经济、调节社会分配的职能。中国每年财政收入的90%以上来自税收,其地位和作用越来越重要,可称之为国家经济的“晴雨表”,有效进行税务管理、充分挖掘税务大数…...
JFET(结型场效应管)
JFET的结构示意图 参考:https://blog.csdn.net/weixin_45882303/article/details/106008695 下图是实际结构图, 下面是原理图和符号表示(参考连接中的图片) 分析 VGS 对电压id的控制(固定VDS) 当让D和…...
oceanbase部署--使用OBD部署obagent和promethous_grafana软件
obagent OBAgent 通常部署在 OBServer 节点上。OBAgent支持推、拉两种数据采集模式,可以满足不同的应用场景。 OBAgent默认支持的插件包括主机数据采集、OceanBase 数据库指标的采集、监控数据标签处理和 Prometheus 协议的 HTTP 服务。 1)编辑 OBAgent …...
wordpress怎么设置页面导航/营销渠道管理
一 GTID的介绍 GTID是一个基于原始mysql服务器生成的一个已经被成功执行的全局事务ID,它由服务器ID以及事务ID组合而成。这个全局事务ID不仅仅在原始服务器器上唯一,在所有存在主从关系 的mysql服务器上也是唯一的。正是因为这样一个特性使得mysql的主从…...
wordpresscom下载/口碑营销的概念是什么
http://www.hankcs.com/nlp/hmm-and-segmentation-tagging-named-entity-recognition.html HMM(隐马尔可夫模型)是用来描述隐含未知参数的统计模型,举一个经典的例子:一个东京的朋友每天根据天气{下雨,天晴}决定当天的…...
盘锦做网站选哪家好/公司软文
attr()方法是用于设置标签的属性,比如src,href,title;(这些更多的是元素的基本属性,HTML的属性); 目录 一:操作元素属性 (1)attr()方法&#x…...
怎么自己做网站模板/谷歌推广费用多少
首先安装LNMP环境,要求PHP-5.3以上版本.参考:http://isadba.com/?p82 或者参考 http://isadba.com/?p572然后下载Anemometergit clone https://github.com/box/Anemometer.git anemometer配置LNMP将下载下来的anemometer部署到LAMP上面,可以通过web打开页面,会提示你没有配置…...
企业所得税怎么缴纳/杭州seo网络公司
小样本学习&元学习经典论文整理||持续更新 核心思想 本文设计了一种利用孪生网络结构解决小样本分类任务的方法,其思想非常简单,用相同的网络结构分别对两幅图像提取特征,如果两幅图像的特征信息非常接近(L1距离小࿰…...
动易sf做网站多少钱/seo关键词优化外包
Flink Standalone HA https://ci.apache.org/projects/flink/flink-docs-release-1.13/docs/deployment/ha/zookeeper_ha/...