Vue源码系列讲解——指令篇【一】(自定义指令)
目录
1. 前言
2. 何时生效
3. 指令钩子函数
4. 如何生效
5. 总结
1. 前言
在Vue中,除了Vue本身为我们提供的一些内置指令之外,Vue还支持用户自定义指令。并且用户有两种定义指令的方式:一种是使用全局API——Vue.directive来定义全局指令,这种方式定义的指令会被存放在Vue.options['directives']中;另一种是在组件内的directive选项中定义专为该组件使用的局部指令,这种方式定义的指令会被存放在vm.$options['directives']中。
可以看到,无论是使用哪一种方式定义的指令它都是将定义好的指令存放在指定的地方,而并不能让指令生效。那么定义的指令什么时候才会生效呢?或者说它是如何生效的呢?本篇文章就来带你探究自定义指令如何生效的内部原理。
2. 何时生效
我们知道,指令是作为标签属性写在模板中的HTML标签上的,那么又回到那句老话了,既然是写在模板中的,那它必然会经过模板编译,编译之后会产生虚拟DOM,在虚拟DOM渲染更新时,除了更新节点的内容之外,节点上的一些指令、事件等内容也需要更新。另外,我们还知道,虚拟DOM节点的更新不只是更新一个已有的节点,也有可能是创建一个新的节点,还有可能是删除一个节点等等,这些都叫做虚拟DOM节点的更新,那么既然虚拟DOM节点更新的概念这么大,那到底该什么时候处理指令的相关逻辑,执行指令函数,让指令生效呢?
其实,在虚拟DOM渲染更新的时候,它在执行相关操作的同时,还会在每个阶段触发相应的钩子函数,我们只需监听不同的钩子函数,就可以在虚拟DOM渲染更新的不同阶段做一些额外的事情。下表给出了虚拟DOM在渲染更新的不同阶段所触发的不同的钩子函数及其触发时机:
| 钩子函数名称 | 触发时机 | 回调参数 |
|---|---|---|
| init | 已创建VNode,在patch期间发现新的虚拟节点时被触发 | VNode |
| create | 已基于VNode创建了DOM元素 | emptyNode和VNode |
| activate | keep-alive组件被创建 | emptyNode和innerNode |
| insert | VNode对应的DOM元素被插入到父节点中时被触发 | VNode |
| prepatch | 一个VNode即将被patch之前触发 | oldVNode和VNode |
| update | 一个VNode更新时触发 | oldVNode和VNode |
| postpatch | 一个VNode被patch完毕时触发 | oldVNode和VNode |
| destory | 一个VNode对应的DOM元素从DOM中移除时或者它的父元素从DOM中移除时触发 | VNode |
| remove | 一个VNode对应的DOM元素从DOM中移除时触发。与destory不同的是,如果是直接将该VNode的父元素从DOM中移除导致该元素被移除,那么不会触发 | VNode和removeCallback |
所以我们只需在恰当的阶段监听对应的钩子函数来处理指令的相关逻辑,从而就可以使指令生效了。
现在我们来设想一下,在什么阶段处理指令的逻辑会比较合适?仔细想一下,当一个节点被创建成DOM元素时,如果这个节点上有指令,那此时得处理指令逻辑,让指令生效;当一个节点被更新时,如果节点更新之前没有指令,而更新之后有了指令,或者是更新前后节点上的指令发生了变化,那此时得处理指令逻辑,让指令生效;另外,当节点被移除时,那节点上的指令自然也就没有用了,此时还得处理指令逻辑。
基于以上设想,我们得出一个结论:在虚拟DOM渲染更新的create、update、destory阶段都得处理指令逻辑,所以我们需要监听这三个钩子函数来处理指令逻辑。事实上,Vue也是这么做的,代码如下:
export default {create: updateDirectives,update: updateDirectives,destroy: function unbindDirectives (vnode: VNodeWithData) {updateDirectives(vnode, emptyNode)}
}
可以看到,分别监听了这三个钩子函数,当虚拟DOM渲染更新的时候会触发这三个钩子函数,从而就会执行updateDirectives函数,在该函数内部就会去处理指令的相关逻辑,我们在下面会详细分析该函数内部是如何处理指令逻辑。
3. 指令钩子函数
Vue对于自定义指令定义对象提供了几个钩子函数,这几个钩子函数分别对应着指令的几种状态,一个指令从第一次被绑定到元素上到最终与被绑定的元素解绑,它会经过以下几种状态:
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
有了每个状态的钩子函数,这样我们就可以让指令在不同状态下做不同的事情。
例如,我们想让指令所绑定的输入框一插入到 DOM 中,输入框就获得焦点,那么,我们就可以这样定义指令:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {// 当被绑定的元素插入到 DOM 中时……inserted: function (el) {// 聚焦元素el.focus()}
})
在模板中使用该指令,如下:
<input v-focus>
可以看到,我们在定义该指令的时候,我们将获取焦点的逻辑写在了inserted钩子函数里面,这样就保证了当被绑定的元素插入到父节点时,获取焦点的逻辑就会被执行。
同理,我们也可以在一个指令中设置多个钩子函数,从而让一个指令在不同状态下做不同的事。
OK,有了这个概念之后,接下来我们就来分析指令是如何生效的。
4. 如何生效
在第二章节中我们知道了,当虚拟DOM渲染更新的时候会触发create、update、destory这三个钩子函数,从而就会执行updateDirectives函数来处理指令的相关逻辑,执行指令函数,让指令生效。所以,探究指令如何生效的问题就是分析updateDirectives函数的内部逻辑。
updateDirectives函数的定义位于源码的src/core/vdom/modules/directives.js文件中,如下:
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {if (oldVnode.data.directives || vnode.data.directives) {_update(oldVnode, vnode)}
}
可以看到,该函数的内部是判断了如果新旧VNode中只要有一方涉及到了指令,那就调用_update方法去处理指令逻辑。
_update方法定义如下:
function _update (oldVnode, vnode) {const isCreate = oldVnode === emptyNodeconst isDestroy = vnode === emptyNodeconst oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)const dirsWithInsert = []const dirsWithPostpatch = []let key, oldDir, dirfor (key in newDirs) {oldDir = oldDirs[key]dir = newDirs[key]if (!oldDir) {// new directive, bindcallHook(dir, 'bind', vnode, oldVnode)if (dir.def && dir.def.inserted) {dirsWithInsert.push(dir)}} else {// existing directive, updatedir.oldValue = oldDir.valuedir.oldArg = oldDir.argcallHook(dir, 'update', vnode, oldVnode)if (dir.def && dir.def.componentUpdated) {dirsWithPostpatch.push(dir)}}}if (dirsWithInsert.length) {const callInsert = () => {for (let i = 0; i < dirsWithInsert.length; i++) {callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)}}if (isCreate) {mergeVNodeHook(vnode, 'insert', callInsert)} else {callInsert()}}if (dirsWithPostpatch.length) {mergeVNodeHook(vnode, 'postpatch', () => {for (let i = 0; i < dirsWithPostpatch.length; i++) {callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)}})}if (!isCreate) {for (key in oldDirs) {if (!newDirs[key]) {// no longer present, unbindcallHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)}}}
}
可以看到,该方法内首先定义了一些变量,如下:
const isCreate = oldVnode === emptyNode
const isDestroy = vnode === emptyNode
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)const dirsWithInsert = []
const dirsWithPostpatch = []
- isCreate:判断当前节点
vnode对应的旧节点oldVnode是不是一个空节点,如果是的话,表明当前节点是一个新创建的节点。 - isDestroy:判断当前节点
vnode是不是一个空节点,如果是的话,表明当前节点对应的旧节点将要被销毁。 - oldDirs:旧的指令集合,即
oldVnode中保存的指令。 - newDirs:新的指令集合,即
vnode中保存的指令。 - dirsWithInsert:保存需要触发
inserted指令钩子函数的指令列表。 - dirsWithPostpatch:保存需要触发
componentUpdated指令钩子函数的指令列表。
另外,你可能还看到了在定义新旧指令集合的变量中调用了normalizeDirectives函数,其实该函数是用来模板中使用到的指令从存放指令的地方取出来,并将其格式进行统一化,其定义如下:
function normalizeDirectives (dirs,vm): {const res = Object.create(null)if (!dirs) {return res}let i, dirfor (i = 0; i < dirs.length; i++) {dir = dirs[i]if (!dir.modifiers) {dir.modifiers = emptyModifiers}res[getRawDirName(dir)] = dirdir.def = resolveAsset(vm.$options, 'directives', dir.name, true)}return res
}
以第三章节中的v-focus指令为例,通过normalizeDirectives函数取出的指令会变成如下样子:
{'v-focus':{name : 'focus' , // 指令的名称value : '', // 指令的值arg:'', // 指令的参数modifiers:{}, // 指令的修饰符def:{inserted:fn}}
}
OK,言归正传,获取到oldDirs和newDirs之后,接下来要做的事情就是对比这两个指令集合并触发对应的指令钩子函数。
首先,循环newDirs,并分别从oldDirs和newDirs取出当前循环到的指令分别保存在变量oldDir和dir中,如下:
let key, oldDir, dir
for (key in newDirs) {oldDir = oldDirs[key]dir = newDirs[key]
}
然后判断当前循环到的指令名key在旧的指令列表oldDirs中是否存在,如果不存在,说明该指令是首次绑定到元素上的一个新指令,此时调用callHook触发指令中的bind钩子函数,接着判断如果该新指令在定义时设置了inserted钩子函数,那么将该指令添加到dirsWithInsert中,以保证执行完所有指令的bind钩子函数后再执行指令的inserted钩子函数,如下:
// 判断当前循环到的指令名`key`在旧的指令列表`oldDirs`中是否存在,如果不存在,那么说明这是一个新的指令
if (!oldDir) {// new directive, bind// 触发指令中的`bind`钩子函数callHook(dir, 'bind', vnode, oldVnode)// 如果定义了inserted 时的钩子函数 那么将该指令添加到dirsWithInsert中if (dir.def && dir.def.inserted) {dirsWithInsert.push(dir)}
}
如果当前循环到的指令名key在旧的指令列表oldDirs中存在时,说明该指令在之前已经绑定过了,那么这一次的操作应该是更新指令。
首先,在dir上添加oldValue属性和oldArg属性,用来保存上一次指令的value属性值和arg属性值,然后调用callHook触发指令中的update钩子函数,接着判断如果该指令在定义时设置了componentUpdated钩子函数,那么将该指令添加到dirsWithPostpatch中,以保证让指令所在的组件的VNode及其子VNode全部更新完后再执行指令的componentUpdated钩子函数,如下:
else {// existing directive, updatedir.oldValue = oldDir.valuedir.oldArg = oldDir.argcallHook(dir, 'update', vnode, oldVnode)if (dir.def && dir.def.componentUpdated) {dirsWithPostpatch.push(dir)}
}
最后,判断dirsWithInsert数组中是否有元素,如果有,则循环dirsWithInsert数组,依次执行每一个指令的inserted钩子函数,如下:
if (dirsWithInsert.length) {const callInsert = () => {for (let i = 0; i < dirsWithInsert.length; i++) {callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)}}
}
从上述代码中可以看到,并没有直接去循环执行每一个指令的inserted钩子函数,而是新创建了一个callInsert函数,当执行该函数的时候才会去循环执行每一个指令的inserted钩子函数。这又是为什么呢?
这是因为指令的inserted钩子函数必须在被绑定元素插入到父节点时调用,那么如果是一个新增的节点,如何保证它已经被插入到父节点了呢?我们之前说过,虚拟DOM在渲染更新的不同阶段会触发不同的钩子函数,比如当DOM节点在被插入到父节点时会触发insert函数,那么我们就知道了,当虚拟DOM渲染更新的insert钩子函数被调用的时候就标志着当前节点已经被插入到父节点了,所以我们要在虚拟DOM渲染更新的insert钩子函数内执行指令的inserted钩子函数。也就是说,当一个新创建的元素被插入到父节点中时虚拟DOM渲染更新的insert钩子函数和指令的inserted钩子函数都要被触发。既然如此,那就可以把这两个钩子函数通过调用mergeVNodeHook方法进行合并,然后统一在虚拟DOM渲染更新的insert钩子函数中触发,这样就保证了元素确实被插入到父节点中才执行的指令的inserted钩子函数,如下:
if (dirsWithInsert.length) {const callInsert = () => {for (let i = 0; i < dirsWithInsert.length; i++) {callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)}}if (isCreate) {mergeVNodeHook(vnode, 'insert', callInsert)} else {callInsert()}
}
同理,我们也需要保证指令所在的组件的VNode及其子VNode全部更新完后再执行指令的componentUpdated钩子函数,所以我们将虚拟DOM渲染更新的postpatch钩子函数和指令的componentUpdated钩子函数进行合并触发,如下:
if (dirsWithPostpatch.length) {mergeVNodeHook(vnode, 'postpatch', () => {for (let i = 0; i < dirsWithPostpatch.length; i++) {callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)}})
}
最后,当newDirs循环完毕后,再循环oldDirs,如果某个指令存在于旧的指令列表oldDirs而在新的指令列表newDirs中不存在,那说明该指令是被废弃的,所以则触发指令的unbind钩子函数对指令进行解绑。如下:
if (!isCreate) {for (key in oldDirs) {if (!newDirs[key]) {// no longer present, unbindcallHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)}}
}
以上就是指令生效的全部逻辑。所谓让指令生效,其实就是在合适的时机执行定义指令时所设置的钩子函数。
5. 总结
本篇文章介绍了关于自定义指令如何生效的相关内容。
首先,我们知道了如果一个DOM节点上绑定了指令,那么在这个DOM节点所对应虚拟DOM节点进行渲染更新的时候,不但会处理节点渲染更新的逻辑,还会处理节点上指令的相关逻辑。具体处理指令逻辑的时机是在虚拟DOM渲染更新的create、update、destory阶段。
接着,我们介绍了Vue对于自定义指令定义对象提供了几个钩子函数,这几个钩子函数分别对应着指令的几种状态,我们可以根据实际的需求将指令逻辑写在合适的指令状态钩子函数中,比如,我们想让指令所绑定的元素一插入到DOM中就执行指令逻辑,那我们就应该把指令逻辑写在指令的inserted钩子函数中。
接着,我们逐行分析了updateDirectives函数,在该函数中就是对比新旧两份VNode上的指令列表,通过对比的异同点从而执行指令不同的钩子函数,让指令生效。
最后,一句话概括就是:所谓让指令生效,其实就是在合适的时机执行定义指令时所设置的钩子函数。
相关文章:
Vue源码系列讲解——指令篇【一】(自定义指令)
目录 1. 前言 2. 何时生效 3. 指令钩子函数 4. 如何生效 5. 总结 1. 前言 在Vue中,除了Vue本身为我们提供的一些内置指令之外,Vue还支持用户自定义指令。并且用户有两种定义指令的方式:一种是使用全局API——Vue.directive来定义全局指令…...
STM32(14)USART
USART:一种片上外设,用来实现串口通信,就是stm32内部的串口 USART简介 串并转换电路 串行通信和并行通信 串行:一根数据线,逐个比特位发送 为什么要串并转换 移位寄存器 USART的基本模型 通过查询SR(状态寄存器&…...
作业 字符数组-统计和加密
字串中数字个数 描述 输入一行字符,统计出其中数字字符的个数。 输入 一行字符串,总长度不超过255。 输出 输出为1行,输出字符串里面数字字符的个数。 样例 #include <iostream> #include<string.h> using namespace std; int m…...
Codeforces Round 719 (Div. 3)除F2题外补题报告
Codeforces Round 719 Div. 3 除F2题外补题报告 得分情况补题情况错题分析C题题目大意初次思路正解思路正解代码错误原因 D题题目大意初次思路正解思路正解代码错误原因 E题题目大意初次思路正解思路正解代码 F1题题目大意正解思路正解代码 G题题目大意正解思路正解代码 得分情…...
docker本地搭建spark yarn hive环境
docker本地搭建spark yarn hive环境 前言软件版本准备工作使用说明构建基础镜像spark on yarn模式构建on-yarn镜像启动on-yarn集群手动方式自动方式 spark on yarn with hive(derby server)模式构建on-yarn-hive镜像启动on-yarn-hive集群手动方式自动方式 常用示例spark执行sh脚…...
每日学习笔记:C++ 11的Tuple
#include <tuple> Tuple介绍(不定数的值组--可理解为pair的升级版) 定义 创建 取值 初始化 获取tuple元素个数、获取tuple某元素类型、将2个tuple类型串接为1个新tuple类型...
MongoDB聚合运算符;$dateToParts
$dateToParts聚合运算符将日期表达式拆分成多个字段放在一个文档返回,属性有year、month、day、hour、minute、second和millisecond。如果iso8601属性设置为true,返回的各部分用ISO周日期返回,属性分别是:isoWeekYear、isoWeek、i…...
Spring MVC RequestMappingHandlerAdapter原理解析
在Spring MVC框架中,RequestMappingHandlerAdapter是一个核心的组件,负责将请求映射到具体的处理器方法上,并调用这些方法来处理请求。其中,invokeHandlerMethod方法是这个适配器中的一个关键方法,它负责实际调用处理器…...
反射整理学习
目录 1、反射介绍 2、反射API 2.1 获取类对应的字节码的对象(三种) 2.2 常用方法 3、反射的应用 3.1 创建 : 测试物料类 3.2 获取类对象 3.3 获取成员变量 3.4 通过字节码对象获取类的成员方法 3.5 通过字节码对象获取类的构造方法 4、创建对象…...
JavaScript 运算规则详解
在 JavaScript 中,运算规则是非常重要的基础知识,了解这些规则可以帮助我们正确地编写代码并避免一些常见的错误。本教程将详细介绍 JavaScript 中的各种运算规则,包括基本运算符、类型转换、运算优先级等内容。 1. 基本运算符 JavaScript …...
C++篇 语 句
到目前为止,我们只见过两种语句: return 语句和表达式语句。根据语句对执行顺 序的影响,C 语言其余语句大多属于以下 3 大类。 选择语句: if 语句和 switch 语句。循环语句: while 语句, do...while 语句和…...
简洁的在线观影开源项目
公众号:【可乐前端】,每天3分钟学习一个优秀的开源项目,分享web面试与实战知识。 每天3分钟开源 hi,这里是每天3分钟开源,很高兴又跟大家见面了,今天介绍的开源项目简介如下: 仓库名࿱…...
VB超级模块函数VB读写记事本-防止乱码支持UTF-8和GB2312编码
Private Sub Command1_Click() Writein “C:\Users\Administrator\Desktop\1.txt”, “文本文内容” End Sub Private Sub Form_Load() Text1 ReadANSI(“C:\Users\Administrator\Desktop\1.txt”) Text2 ReadUTF8(“C:\Users\Administrator\Desktop\1.txt”) End Sub 写入…...
XSS靶场-DOM型初级关卡
一、环境 XSS靶场 二、闯关 1、第一关 先看源码 使用DOM型,获取h2标签,使用innerHTML将内容插入到h2中 我们直接插入<script>标签试一下 明显插入到h2标签中了,为什么不显示呢?看一下官方文档 尽管插入进去了࿰…...
【嵌入式高级C语言】10:C语言文件
文章目录 1 文件的概述1.1 文件分类(存储介质)1.2 磁盘文件分类(存储方式)1.3 二进制文件和文本文件的区别 2 文件缓冲区3 文件指针4 文件的API4.1 打开文件4.2 关闭文件4.3 重新定位流4.3.1 fseek4.3.2 ftell4.3.3 rewind 4.4 字…...
创建数据表
Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209645 如果要进行数据表的创建 create table 表名称 (列名称 类型 [DEFAULT 默认值 ] ,列名称 类型 [DEFAULT 默认值 ] ,列名称 类型 [DEFAULT 默认值 ] ,...列名称 类型 [DEFAULT 默认值 ] )…...
C语言字符串型常量
在C语言中,字符串型常量是由一系列字符组成的常量。字符串常量在C中以双引号(")括起来,例如:“Hello, World!”。字符串常量在C中是不可变的,也就是说,一旦定义,就不能修改其内…...
计算机网络 八股
计算机网络体系结构 OSI:物理层、数据链路层、网络层、运输层、会话层、表示层、应用层...
深入了解 Jetpack Compose 中的 Modifier
Jetpack Compose 是 Android 中用于构建用户界面的现代化工具包。其中,Modifier 是一个非常重要的概念,它允许我们对 UI 组件进行各种样式和布局的调整。在本篇博客中,我们将深入了解 Modifier,以及如何在 Compose 中使用它。 什…...
【数据库】聚合函数|group by分组|having|where|排序|函数 关键字的使用
目录 一、聚合函数 1、max() 2、min() 3、avg() 4、sum() 5、count() 二、group by 分组汇总 一般聚合函数配合着group by(分组)语句进行使用 把一组的数据放到一起,再配合聚合函数进行使用 三、having having语句 做筛选的 四、where和having的作用以及区…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...
全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...
水泥厂自动化升级利器:Devicenet转Modbus rtu协议转换网关
在水泥厂的生产流程中,工业自动化网关起着至关重要的作用,尤其是JH-DVN-RTU疆鸿智能Devicenet转Modbus rtu协议转换网关,为水泥厂实现高效生产与精准控制提供了有力支持。 水泥厂设备众多,其中不少设备采用Devicenet协议。Devicen…...
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
文章目录 1. 题目描述1.1 链表节点定义 2. 理解题目2.1 问题可视化2.2 核心挑战 3. 解法一:HashSet 标记访问法3.1 算法思路3.2 Java代码实现3.3 详细执行过程演示3.4 执行结果示例3.5 复杂度分析3.6 优缺点分析 4. 解法二:Floyd 快慢指针法(…...
ArcPy扩展模块的使用(3)
管理工程项目 arcpy.mp模块允许用户管理布局、地图、报表、文件夹连接、视图等工程项目。例如,可以更新、修复或替换图层数据源,修改图层的符号系统,甚至自动在线执行共享要托管在组织中的工程项。 以下代码展示了如何更新图层的数据源&…...
