21_双端 diff 算法

目录

    • 双端比较的原理
    • 非理想状况的处理方式
    • 添加新元素
    • 移除不存在的元素

在上一节中,我们实现了简单的 diff 算法,简单的 diff 算法利用 key 属性,尽可能的复用 DOM 元素,并通过移动 DOM 元素来完成更新,从而减少不断创建和销毁 DOM 元素带来的性能开销,但是简单 diff 算法依然存在很多缺陷,这些缺陷可以通过双端 diff 算法来解决。

双端比较的原理

简单 diff 算法的问题在于,它的移动操作并不是最优的,我们还是使用上一节的例子来看,如图:

在这里插入图片描述

在这个例子中,我们使用简单 diff 算法来更新需要进行两次 DOM 移动,将 p-1 移动到 p-3 后面,将 p-2移动到 p-1 后面。

但是其实只需要一次移动 DOM 即可,就是将 p-3 移动到 p-1 的前面即可。这一点想要实现,是简单 diff 算法无法做到的,而使用双端 diff 算法就可以。

顾名思义,双端 diff 算法是一种同事对新旧两组子节点的两个端点进行比较的算法,因此我们需要四个索引值,分别指向新旧两组子节点的端点,如图:

在这里插入图片描述

现在我们就将之前简单 diff 算法的代码清空,来编写双端 diff 算法的代码,并将其封装到 patchKeyedChildren 函数中,首先我们需要四个索引值,和四个索引值指向的 vnode,如下:

function patchChildren(n1, n2, container) {if (isString(n2.children)) {// 省略} else if (isArray(n2.children)) {if (isArray(n1.children)) {const oldChildren = n1.childrenconst newChildren = n2.childrenif (newChildren[0].key) {patchKeyedChildren(oldChildren, newChildren, container)} else {patchUnkeyedChildren(oldChildren, newChildren, container)}} else {// 省略}} else {// 省略}
}function patchKeyedChildren(c1, c2, container) {// 四个索引值let oldStartIdx = 0let oldEndIdx = c1.length - 1let newStartIdx = 0let newEndIdx = c2.length - 1// 四个节点let oldStartVNode = c1[oldStartIdx]let oldEndVNode = c1[oldEndIdx]let newStartVNode = c2[newStartIdx]let newEndVNode = c2[newEndIdx]
}

双端比较的步骤如图所示,如图:

在这里插入图片描述

在双端比较中,一共分为了四步,如图中的连线所示。

  • 第一步:比较 oldChildren 中的第一个子节点 p-1 与 newChildren 第一个子节点 p-4 进行对比,两者 key 不一致,不可复用,所以什么都不做。
  • 第二步:比较 oldChildren 中的最后一个子节点 p-4 与 newChildren 最后一个子节点 p-3 进行对比,两者 key 不一致,不可复用,所以什么都不做。
  • 第三步:比较 oldChildren 中的第一个子节点 p-1 与 newChildren 最后一个子节点 p-3 进行对比,两者 key 不一致,不可复用,所以什么都不做。
  • 第四步:比较 oldChildren 中的最后一个子节点 p-4 与 newChildren 第一个子节点 p-4 进行对比,key 值一样,可以进行复用,因此 DOM 可以进行复用。

而通过这个比较,我们不难发现: 节点 p-4 原本是最后一个子节点,但在新的顺序中,是第一个子节点。换句话说:将索引 oldEndIdx 指向的虚拟节点所对应的真实 DOM 移动到索引 oldStartIdx 指向的虚拟节点所对应的真实 DOM 前面。如下:

function patchKeyedChildren(c1, c2, container) {let oldStartIdx = 0let oldEndIdx = c1.length - 1let newStartIdx = 0let newEndIdx = c2.length - 1let oldStartVNode = c1[oldStartIdx]let oldEndVNode = c1[oldEndIdx]let newStartVNode = c2[newStartIdx]let newEndVNode = c2[newEndIdx]if (isSameVNodeType(oldStartVNode, newStartVNode)) {// 第一步} else if (isSameVNodeType(oldEndVNode, newEndVNode)) {// 第二步} else if (isSameVNodeType(oldStartVNode, newEndVNode)) {// 第三步} else if (isSameVNodeType(oldEndVNode, newStartVNode)) {// 第四步// 依然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动 DOM 操作// 将旧的子节点中的最后一个节点移动到第一个节点之前hostInsert(oldEndVNode.el, container, oldStartVNode.el)// 移动完毕后,更新索引值,并指向下一个位置oldEndVNode = c1[--oldEndIdx]newStartVNode = c2[++newStartIdx]}
}

在这段代码中,我们可以发现除了上述分析的步骤之外,还更新了索引值,这是因为只进行一次,还没有将新的顺序都移动到正确的位置上,所以我们需要更新索引进行下一次循环,这里修改 oldEndIdx 和 newStartIdx 是因为这次移动的节点分别是 newChildren 的头部节点和 oldChildren 的尾部节点,经过这一次更新之后,新旧两组子节点与真实 DOM 节点关系如图:

在这里插入图片描述

从此图可以看出下一次双端 diff 比较的步骤以及目前真实 DOM 节点的顺序,而为了让其再次执行,我们需要使用一个 while 循环来开启下一次的比较,并套用上一步的步骤,我们会发现执行到第二步的时候新旧两组节点的 key 就是相等,也就意味着可以服用,不要移动,但是为了保证下一次比较的进行,还需要更新索引,如下:

function patchKeyedChildren(c1, c2, container) {// 省略// 设置循环条件,防止数组越界while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameVNodeType(oldStartVNode, newStartVNode)) {// 第一步} else if (isSameVNodeType(oldEndVNode, newEndVNode)) {// 第二步// 新节点顺序也处于尾部,不需要移动,但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引值oldEndVNode = c1[--oldEndIdx]newEndVNode = c2[--newEndIdx]} else if (isSameVNodeType(oldStartVNode, newEndVNode)) {// 第三步} else if (isSameVNodeType(oldEndVNode, newStartVNode)) {// 第四步patch(oldEndVNode, newStartVNode, container)hostInsert(oldEndVNode.el, container, oldStartVNode.el)oldEndVNode = c1[--oldEndIdx]newStartVNode = c2[++newStartIdx]}}
}

这次更新完成之后,关系如图:

在这里插入图片描述

在这个状态下,再次进行比较,执行到第三步的时候会发现,newChild 的 p-1 与 oldChildren 的 p-1 是可复用的,那么同理,移动 DOM,而第三步的移动,是需要将 oldChildren 的头部节点对应的真实 DOM 移动到 oldChildren 的尾部节点对应的真实 DOM 后面,然后更新索引,如下:

function patchKeyedChildren(c1, c2, container) {// 省略// 设置循环条件,防止数组越界while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameVNodeType(oldStartVNode, newStartVNode)) {// 第一步} else if (isSameVNodeType(oldEndVNode, newEndVNode)) {// 第二步patch(oldEndVNode, newEndVNode, container)oldEndVNode = c1[--oldEndIdx]newEndVNode = c2[--newEndIdx]} else if (isSameVNodeType(oldStartVNode, newEndVNode)) {// 第三步// 打补丁patch(oldStartVNode, newEndVNode, container)// 移动 DOM,将 oldStartVNode.el 移动到 oldEndVNode.el 的下一个兄弟节点之前// - 此时 oldEndVNode 是相对遍历比较的最后一个节点,而非是真实 DOM 的最后一个节点hostInsert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)// 更新索引oldStartVNode = c1[++oldStartIdx]newEndVNode = c2[--newEndIdx]} else if (isSameVNodeType(oldEndVNode, newStartVNode)) {// 第四步patch(oldEndVNode, newStartVNode, container)hostInsert(oldEndVNode.el, container, oldStartVNode.el)oldEndVNode = c1[--oldEndIdx]newStartVNode = c2[++newStartIdx]}}
}

这次比较之后,就只存在一个节点了,关系如图:

在这里插入图片描述

而此时将会进行最后一次遍历,即进入第一步,如下:

function patchKeyedChildren(c1, c2, container) {// 省略// 设置循环条件,防止数组越界while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameVNodeType(oldStartVNode, newStartVNode)) {// 第一步patch(oldStartVNode, newStartVNode, container)// 更新索引值oldStartVNode = c1[++oldStartIdx]newStartVNode = c2[++newStartIdx]} else if (isSameVNodeType(oldEndVNode, newEndVNode)) {// 第二步patch(oldEndVNode, newEndVNode, container)oldEndVNode = c1[--oldEndIdx]newEndVNode = c2[--newEndIdx]} else if (isSameVNodeType(oldStartVNode, newEndVNode)) {// 第三步patch(oldStartVNode, newEndVNode, container)hostInsert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode = c1[++oldStartIdx]newEndVNode = c2[--newEndIdx]} else if (isSameVNodeType(oldEndVNode, newStartVNode)) {// 第四步patch(oldEndVNode, newStartVNode, container)hostInsert(oldEndVNode.el, container, oldStartVNode.el)oldEndVNode = c1[--oldEndIdx]newStartVNode = c2[++newStartIdx]}}
}

此时,新旧两组子节点都遍历完毕,真实 DOM 元素也正确的移动了位置。

非理想状况的处理方式

在上述的例子中,我们采用的案例是一个理想的情况,每一次对比都可以命中四步中的任意一步,但实际中不可能总会出现这种理想情况,例如:

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

当我们的对比情况是上面这种情况的时候,开始第一次的双端对比,发现无法命中四步中的任何一步,所以我们还需要开一个分支来处理这个额外的情况。

具体的处理方法就是,使用 newChildren 中的头部节点去 oldChildren 中去寻找,如果找到了,就通过找到的 index 在 oldChildren 中找到对应的元素,然后在将这个元素移动到最前面,最后将原来这个位置 vnode 设置为 undefined,并更新索引,如下:

function patchKeyedChildren(c1, c2, container) {// 省略while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameVNodeType(oldStartVNode, newStartVNode)) {// 第一步-省略} else if (isSameVNodeType(oldEndVNode, newEndVNode)) {// 第二步-省略} else if (isSameVNodeType(oldStartVNode, newEndVNode)) {// 第三步-省略} else if (isSameVNodeType(oldEndVNode, newStartVNode)) {// 第四步-省略} else {// 处理其他情况const idxInOld = c1.findIndex(node => {return isSameVNodeType(node, newStartVNode)})// 如果大于 0 则是找到了,等于 0 是不存在的,因为前面四步就校验过了if (idxInOld > 0) {const vnodeToMove = c1[idxInOld]// 打补丁patch(vnodeToMove, newStartVNode, container)// 移动 DOMhostInsert(vnodeToMove.el, container, oldStartVNode.el)// 由于 idxInOld 位置的节点已经移动,所以需要设置为 undefinedc1[idxInOld] = undefined// 更新索引newStartVNode = c2[++newStartIdx]}}}
}

处理之后的情况如图:

在这里插入图片描述

此时真实 DOM 的顺序为:p-2、p-1、p-3、p-4,然后继续进行双端的 diff 算法,如图:

在这里插入图片描述

而按照之前的比较逻辑,会在第四步比对成功,所以会进行一次移动,而第四步是取的 newChildren 的头部节点,则表示这个 oldChildren 的尾部节点要移动到当前 oldStartVNode 的前面,移动之后的关系和下一轮比较步骤如图:

在这里插入图片描述

此时真实 DOM 的顺序为:p-2、p-4、p-1、p-3,然后开始进行比较,此时就会执行第一步,而第一步是不需要移动的,只需要更新索引即可,更新索引之后的关系和下一轮比较步骤如图:

在这里插入图片描述

此时不难发现,有一些不同于之前的地方,即 oldChildren 的头部节点是 undefined,表示处理过了,我们就需要跳过。因此我们需要补充这部分的代码,如下:

function patchKeyedChildren(c1, c2, container) {// 省略while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 增加两个判断分支,如果头尾部节点为 undefined,表示处理过了,则跳过if (!oldStartVNode) {oldStartVNode = c1[++oldStartIdx]} else if (!oldEndVNode) {oldEndVNode = c1[--oldEndIdx]} else if (isSameVNodeType(oldStartVNode, newStartVNode)) {// 第一步-省略} else if (isSameVNodeType(oldEndVNode, newEndVNode)) {// 第二步-省略} else if (isSameVNodeType(oldStartVNode, newEndVNode)) {// 第三步-省略} else if (isSameVNodeType(oldEndVNode, newStartVNode)) {// 第四步-省略} else {// 处理其他情况const idxInOld = c1.findIndex(node => {return isSameVNodeType(node, newStartVNode)})if (idxInOld > 0) {const vnodeToMove = c1[idxInOld]patch(vnodeToMove, newStartVNode, container)hostInsert(vnodeToMove.el, container, oldStartVNode.el)c1[idxInOld] = undefinednewStartVNode = c2[++newStartIdx]}}}
}

这一步处理完成之后,跳过了 undefined 节点,四个步骤又重合了,继续对比的逻辑,就完成了排序。

添加新元素

我们来看一下新增元素的例子,如下:

  • oldChildren:p-1、p-2、p-3
  • newChildren:p-4、p-1、p-3、p-2

这个例子中,我们需要将 p-4 进行新增,首先我们按照双端比较的四个步骤,去对比,发现 newChildren 的 p-4 节点都无法找到,然后再去 oldChildren 遍历查找,也没有找到,则说明这个节点需要进行新增。

而挂载到哪里呢,此时 p-4 为 newStartVNode,表示在 newChildren 中为头部节点,则需要加入到当前 oldStartVNode 对应的真实 DOM 之前,如下:

function patchKeyedChildren(c1, c2, container) {// 省略while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (!oldStartVNode) {oldStartVNode = c1[++oldStartIdx]} else if (!oldEndVNode) {oldEndVNode = c1[--oldEndIdx]} else if (isSameVNodeType(oldStartVNode, newStartVNode)) {// 第一步-省略} else if (isSameVNodeType(oldEndVNode, newEndVNode)) {// 第二步-省略} else if (isSameVNodeType(oldStartVNode, newEndVNode)) {// 第三步-省略} else if (isSameVNodeType(oldEndVNode, newStartVNode)) {// 第四步-省略} else {// 处理其他情况const idxInOld = c1.findIndex(node => {return isSameVNodeType(node, newStartVNode)})if (idxInOld > 0) {const vnodeToMove = c1[idxInOld]patch(vnodeToMove, newStartVNode, container)hostInsert(vnodeToMove.el, container, oldStartVNode.el)c1[idxInOld] = undefined} else {// 如果没有找到,则创建新节点,并传入锚点patch(null, newStartVNode, container, oldStartVNode.el)}newStartVNode = c2[++newStartIdx]}}
}

这样就可以进行挂载了,但是这样真的完美吗?我们来看一下下面这个例子,如下:

  • oldChildren:p-1、p-2、p-3
  • newChildren:p-4、p-1、p-2、p-3

可以看到,和上一个例子的不同之处只在于,更换了一下顺序,这样改动之后,我们需要对比的步骤如图:

在这里插入图片描述

在这个步骤中,我们执行到第二步的时候,就找到了可以服用的节点,不过由于都是尾部节点,所以无需移动,只需要进行索引的更新即可,此时两组子节点关系和下一次比较步骤如图:

在这里插入图片描述

此时我们可以发现,后续的两次比对都是与上一次是一样的,所以我们这里省略 p-2、p-1 的对比,直接来到完成 p-1 对比之后的状态,如图:

在这里插入图片描述

现在我们可以发现,oldStartIdx 大于了 oldEndIdx,就会导致无法进入 while 循环,自然无法执行挂载逻辑,所以我们还需要进行一次处理,如下:

function patchKeyedChildren(c1, c2, container) {// 省略while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 省略}// 循环结束之后检查索引值的情况if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {// oldEndIdx 小于 oldStartIdx,表示旧节点遍历完了// newStartIdx 小于或等于 newEndIdx,表示还有新节点没有处理,需要进行挂载// 可能存在多个,所以要使用 for 循环来处理for (let i = newStartIdx; i <= newEndIdx; i++) {// 锚点:如果 newEndIdx 下一个节点存在,则使用下一个节点,否则使用 null//  - 为 null 则表示这个多余的节点是处于尾部const anchor = c2[newEndIdx + 1] ? c2[newEndIdx + 1].el : nullpatch(null, c2[i], container, anchor)}}
}

移除不存在的元素

我们再来看一下移除元素的情况,如图:

在这里插入图片描述

执行第一步的时候就会发现,p-1 是可以复用的,那么将不做处理,下一步需要执行的步骤如图:

在这里插入图片描述

在此图中可以看出,执行第二步的时候,会发现 p-3 可以进行复用,newChildren 的 p-3 为尾部节点,即会将 newEndIdx 进行 --,oldChildren 的 p-3 也是尾部节点,也会将 oldEndIdx 进行 --,又因为都是尾部的原因,无需移动,最后关系如图:

在这里插入图片描述

此时再次执行的时候,就会发现 newEndIdx 小于 newStartIdx,不符合 while 循环的条件,则会结束 while 循环。需要额外增加逻辑来进行处理,如下:

function patchKeyedChildren(c1, c2, container) {// 省略while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 省略}// 循环结束之后检查索引值的情况if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {// 挂载-省略} else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {// newEndIdx 小于 newStartIdx,表示新节点遍历完了// 且如果此时 oldStartIdx 小于或等于 oldEndIdx,表示还有旧节点没有处理,需要进行卸载// 可能存在多个,所以要使用 for 循环来处理for (let i = oldStartIdx; i <= oldEndIdx; i++) {unmount(c1[i])}}
}

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

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

相关文章

微服务实战系列之玩转Docker(十六)

导览 前言Q&#xff1a;基于容器云如何实现高可用的配置中心一、etcd入门1. 简介2. 特点 二、etcd实践1. 安装etcd镜像2. 创建etcd集群2.1 etcd-node12.2 etcd-node22.3 etcd-node3 3. 启动etcd集群 结语系列回顾 前言 Docker&#xff0c;一个宠儿&#xff0c;一个云原生领域的…

注册信息的提交

动态网页是指能够根据用户的操作或输入动态变化的网页。与静态网页相比&#xff0c;动态网页具有交互性和可变性。 一 动态网页概念 动态网页通常使用脚本语言&#xff08;如JavaScript&#xff09;与服务器进行交互&#xff0c;从服务器获取数据并动态更新网页内容。常见的动…

aws 部署测试环境服务+ip域名绑定

aws 部署springboot vue ip域名绑定域名 1.新建实例之后&#xff0c;作为测试环境开放mysql入出站规则&#xff0c;route53域名&#xff0c;红框中放入阿里云域名 1.设置出入站规则 实例应用安全组 2.mysql aws部署&#xff0c;redis,java环境&#xff0c;参见之前文章腾讯…

《数字图像处理基础》学习05-数字图像的灰度直方图

目录 一&#xff0c;数字图像的数值描述 &#xff11;&#xff0c;二值图像 &#xff12;&#xff0c;灰度图像 3&#xff0c;彩色图像 二&#xff0c;数字图像的灰度直方图 一&#xff0c;数字图像的数值描述 在之前的学习中&#xff0c;我知道了图像都是二维信息&…

书生大模型第四期 | L0G3000 git 基础知识

1、破冰行动 fork项目 PR链接&#xff1a;跳转访问 https://github.com/InternLM/Tutorial/pull/21632、构建个人项目 创建一个仓库保存LLM学习的笔记&#xff0c;以md文件为主 博客页面项目

使用 OpenTelemetry 定制跨度名称并丰富跨度而无需更改代码 - 第 1 部分

作者&#xff1a;来自 Elastic David Hope OpenTelemetry Collector 提供强大的功能&#xff0c;可以在遥测数据到达可观察性工具之前丰富和细化遥测数据。在这篇博文中&#xff0c;我们将探讨如何利用 Collector 在 Elastic Observability 中创建更有意义的 transaction 名称&…

成都睿明智科技有限公司正规吗靠谱吗?

在这个短视频风起云涌的时代&#xff0c;抖音电商以其独特的魅力&#xff0c;成为了无数商家竞相追逐的新蓝海。而在这片浩瀚的商海中&#xff0c;成都睿明智科技有限公司犹如一艘装备精良的航船&#xff0c;引领着众多企业破浪前行&#xff0c;探索抖音电商的无限可能。今天&a…

GHuNeRF: Generalizable Human NeRF from a Monocular Video

研究背景 研究问题&#xff1a;这篇文章要解决的问题是学习一个从单目视频中泛化的人类NeRF模型。尽管现有的泛化人类NeRF已经取得了令人印象深刻的成果&#xff0c;但它们需要多视图图像或视频&#xff0c;这在某些情况下可能不可用。此外&#xff0c;一些基于单目视频的人类…

中聚企服:打造智能企业服务助手,“中聚AI”解答一切企业难题

近日&#xff0c;一款专为企业用户设计的智能问答助手——“中聚AI”正式亮相市场。这款AI由中产聚融有限公司旗下的中聚企服团队自主研发&#xff0c;旨在帮助企业用户快速、高效地解答经营过程中的各种难题&#xff0c;覆盖从公司注册、财税规划到知识产权和资质办理等多领域…

手把手教你轻松掌握~Air780E软件UDP应用示例!快来看!

还不会的小伙伴看过来&#xff01;通过本文的介绍&#xff0c;相信大家已经掌握了Air780E模组UDP应用的基本操作和常见问题的解决方法。赶快动手实践吧&#xff0c;让你的项目更加高效稳定&#xff01; 1、UDP概述 UDP&#xff08;用户数据报协议&#xff0c;UserDatagramProt…

Win10搭建SFTP服务器

1、下载安装 Release v9.5.0.0p1-Beta PowerShell/Win32-OpenSSH GitHub 下载OpenSSH-Win64.zip 解压之后放入到&#xff1a;C:\Program Files (x86)\OpenSSH-Win64以管理员身份打开CMD进入到 C:\Program Files (x86)\OpenSSH-Win64 文件夹执行命令 powershell.exe -Exec…

1分钟解决Excel打开CSV文件出现乱码问题

一、编码问题 1、不同编码格式 CSV 文件有多种编码格式&#xff0c;如 UTF - 8、UTF - 16、ANSI 等。如果 CSV 文件是 UTF - 8 编码&#xff0c;而 Excel 默认使用的是 ANSI 编码打开&#xff0c;就可能出现乱码。例如&#xff0c;许多从网络应用程序或非 Windows 系统生成的 …

构建灵活、高效的HTTP/1.1应用:探索h11库

文章目录 构建灵活、高效的HTTP/1.1应用&#xff1a;探索h11库背景这个库是什么&#xff1f;如何安装这个库&#xff1f;库函数使用方法使用场景常见的Bug及解决方案总结 构建灵活、高效的HTTP/1.1应用&#xff1a;探索h11库 背景 在现代网络应用中&#xff0c;HTTP协议是基础…

【算法】C++深度优先搜索(DFS)全解析

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

汽车免拆诊断案例 | 2010款起亚赛拉图车发动机转速表指针不动

故障现象  一辆2010款起亚赛拉图车&#xff0c;搭载G4ED 发动机&#xff0c;累计行驶里程约为17.2万km。车主反映&#xff0c;车辆行驶正常&#xff0c;但组合仪表上的发动机转速表指针始终不动。 故障诊断  接车后进行路试&#xff0c;车速表、燃油存量表及发动机冷却温度…

【环境搭建】Apache ZooKeeper 3.8.4 Stable

软件环境 Ubuntu 20.04 、OpenJDK 11 OpenJDK 11&#xff08;如果已经安装&#xff0c;可以跳过这一步&#xff09; 安装OpenJDK 11&#xff1a; $ sudo apt-get update$ sudo apt-get install -y openjdk-11-jdk 设置 JAVA_HOME 环境变量&#xff1a; $ sudo gedit ~/.bash…

solid works下载

软件安装包下载解压打开 将软件安装包下载到电脑本地&#xff0c;使用解压工具进行解压打开&#xff08;下载解压安装全程关闭杀毒软件及防火墙&#xff09; 打开Crack文件夹 打开Crack文件夹进去 复制SolidWorks_Flexnet_Server文件夹 复制SolidWorks_Flexnet_Server文件夹到…

硅谷甄选(10)用户管理

用户管理模块 9.1 静态搭建 主要是el-form、el-pagination <template><el-card style"height: 80px"><el-form :inline"true" class"form"><el-form-item label"用户名:"><el-input placeholder"请…

C语言-详细讲解-打印各种三角形矩阵

目录 1.字符三角形 2. 直角三角形&#xff08;1&#xff09; 3.直角三角形&#xff08;2&#xff09; 4. 打印乘法口诀表 1.字符三角形 入门级&#xff0c;熟悉输入与输出 #include<stdio.h> int main() {char i;scanf("%c",&i);printf(" %c…

图像的特征类别

一 图像的特征类别 1.自然特征 ①光谱特征 ②几何特征 ③时相特征 2.人工特征 ①直方图特征 ②灰度边缘特征 ③线、角点、纹理特征 图像的特征有很多&#xff0c;按提取特征的范围大小又可分为&#xff1a; ①点特征 仅由各个像素就能决定的性质。如单色图像中的灰度值、彩色…