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的作用以及区…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...

使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...