看不懂来打我!让性能提升56%的Vue3.5响应式重构
前言
在Vue3.5版本中最大的改动就是响应式重构,重构后性能竟然炸裂的提升了56%。之所以重构后的响应式性能提升幅度有这么大,主要还是归功于:双向链表和版本计数。这篇文章我们来讲讲使用双向链表后,Vue内部是如何实现依赖收集和依赖触发的。搞懂了这个之后你就能掌握Vue3.5重构后的响应式原理,至于版本计数如果大家感兴趣可以在评论区留言,关注的人多了欧阳后面会再写一篇版本计数的文章。
加入欧阳的高质量vue源码交流群、欧阳平时写文章参考的多本vue源码电子书
3.5版本以前的响应式
在Vue3.5以前的响应式中主要有两个角色:Sub(订阅者)、Dep(依赖)。其中的订阅者有watchEffect、watch、render函数、computed等。依赖有ref、reactive等响应式变量。
举个例子:
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {dummy1 = counter1.value + counter2.value;console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {dummy2 = counter1.value + counter2.value + 1;console.log("dummy2", dummy2);
});counter1.value++;
counter2.value++;
</script>
在上面的两个watchEffect中都会去监听ref响应式变量:counter1和counter2。
初始化时会分别执行这两个watchEffect中的回调函数,所以就会对里面的响应式变量counter1和counter2进行读操作,所以就会走到响应式变量的get拦截中。
在get拦截中会进行依赖收集(此时的Dep依赖分别是变量counter1和counter2)。
因为在依赖收集期间是在执行watchEffect中的回调函数,所以依赖对应的Sub订阅者就是watchEffect。
由于这里有两个watchEffect,所以这里有两个Sub订阅者,分别对应这两个watchEffect。
在上面的例子中,watchEffect监听了多个ref变量。也就是说,一个Sub订阅者(也就是一个watchEffect)可以订阅多个依赖。
ref响应式变量counter1被多个watchEffect给监听。也就是说,一个Dep依赖(也就是counter1变量)可以被多个订阅者给订阅。
Sub订阅者和Dep依赖他们两的关系是多对多的关系!!!

上面这个就是以前的响应式模型。
新的响应式模型
在Vue3.5版本新的响应式中,Sub订阅者和Dep依赖之间不再有直接的联系,而是新增了一个Link作为桥梁。Sub订阅者通过Link访问到Dep依赖,同理Dep依赖也是通过Link访问到Sub订阅者。如下图:

把上面这个图看懂了,你就能理解Vue新的响应式系统啦。现在你直接看这个图有可能看不懂,没关系,等我讲完后你就能看懂了。
首先从上图中可以看到Sub订阅者和Dep依赖之间没有任何直接的连接关系了,也就是说Sub订阅者不能直接访问到Dep依赖,Dep依赖也不能直接访问Sub订阅者。
Dep依赖我们可以看作是X轴,Sub订阅者可以看作是Y轴,这些Link就是坐标轴上面的坐标。
Vue响应式系统的核心还是没有变,只是多了一个Link,依然还是以前的那一套依赖收集和依赖触发的流程。
在依赖收集的过程中就会画出上面这个图,这个不要急,我接下来会仔细去讲图是如何画出来的。
那么依赖触发的时候又是如何利用上面这种图从而实现触发依赖的呢?我们来看个例子。
上面的这张图其实对应的是我之前举的例子:
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {dummy1 = counter1.value + counter2.value;console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {dummy2 = counter1.value + counter2.value + 1;console.log("dummy2", dummy2);
});counter1.value++;
counter2.value++;
</script>
图中的Dep1依赖对应的就是变量counter1,Dep2依赖对应的就是变量counter2。Sub1订阅者对应的就是第一个watchEffect函数,Sub2订阅者对应的就是第二个watchEffect函数。
当执行counter1.value++时,就会被变量counter1(也就是Dep1依赖)的set函数拦截。从上图中可以看到Dep1依赖有个箭头(对照表中的sub属性)指向Link3,并且Link3也有一个箭头(对照表中的sub属性)指向Sub2。
前面我们讲过了这个Sub2就是对应的第二个watchEffect函数,指向Sub2后我们就可以执行Sub2中的依赖,也就是执行第二个watchEffect函数。这就实现了counter1.value++变量改变后,重新执行第二个watchEffect函数。
执行了第二个watchEffect函数后我们发现Link3在Y轴上面还有一个箭头(对照表中的preSub属性)指向了Link1。同理Link1也有一个箭头(对照表中的sub属性)指向了Sub1。
前面我们讲过了这个Sub1就是对应的第一个watchEffect函数,指向Sub1后我们就可以执行Sub1中的依赖,也就是执行第一个watchEffect函数。这就实现了counter1.value++变量改变后,重新执行第一个watchEffect函数。
至此我们就实现了counter1.value++变量改变后,重新去执行依赖他的两个watchEffect函数。
我们此时再来回顾一下我们前面画的新的响应式模型图,如下图:

我们从这张图来总结一下依赖触发的的规则:
响应式变量Dep1改变后,首先会指向Y轴(Sub订阅者)的队尾的Link节点。然后从Link节点可以直接访问到Sub订阅者,访问到订阅者后就可以触发其依赖,这里就是重新执行对应的watchEffect函数。
接着就是顺着Y轴的队尾向队头移动,每移动到一个新的Link节点都可以指向一个新的Dep依赖,在这里触发其依赖就会重新指向对应的watchEffect函数。
看到这里有的同学有疑问如果是Dep2对应的响应式变量改变后指向Link4,那这个Link4又是怎么指向Sub2的呢?他们中间不是还隔了一个Link3吗?
每一个Link节点上面都有一个sub属性直接指向Y轴上面的Sub依赖,所以这里的Link4有个箭头(对照表中的sub属性)可以直接指向Sub2,然后进行依赖触发。
这就是Vue3.5版本使用双向链表改进后的依赖触发原理,接下来我们会去讲依赖收集过程中是如何将上面的模型图画出来的。
Dep、Sub和Link
在讲Vue3.5版本依赖收集之前,我们先来了解一下新的响应式系统中主要的三个角色:Dep依赖、Sub订阅者、Link节点。
这三个角色其实都是class类,依赖收集和依赖触发的过程中实际就是在操作这些类new出来的的对象。
我们接下来看看这些类中有哪些属性和方法,其实在前面的响应式模型图中我们已经使用箭头标明了这些类上面的属性。
Dep依赖
简化后的Dep类定义如下:
class Dep {// 指向Link链表的尾部节点subs: Link// 收集依赖track: Function// 触发依赖trigger: Function
}
Dep依赖上面的subs属性就是指向队列的尾部,也就是队列中最后一个Sub订阅者对应的Link节点。

比如这里的Dep1,竖向的Link1和Link3就组成了一个队列。其中Link3是队列的队尾,Dep1的subs属性就是指向Link3。
其次就是track函数,对响应式变量进行读操作时会触发。触发这个函数后会进行依赖收集,后面我会讲。
同样trigger函数用于依赖触发,对响应式变量进行写操作时会触发,后面我也会讲。
Sub订阅者
简化后的Sub订阅者定义如下:
interface Subscriber {// 指向Link链表的头部节点deps: Link// 指向Link链表的尾部节点depsTail: Link// 执行依赖notify: Function
}
想必细心的你发现了这里的Subscriber是一个interface接口,而不是一个class类。因为实现了这个Subscriber接口的class类都是订阅者,比如watchEffect、watch、render函数、computed等。

比如这里的Sub1,横向的Link1和Link2就组成一个队列。其中的队尾就是Link2(depsTail属性),队头就是Link1(deps属性)。
还有就是notify函数,执行这个函数就是在执行依赖。比如对于watchEffect来说,执行notify函数后就会执行watchEffect的回调函数。
Link节点
简化后的Link节点类定义如下:
class Link {// 指向Subscriber订阅者sub: Subscriber// 指向Dep依赖dep: Dep// 指向Link链表的后一个节点(X轴)nextDep: Link// 指向Link链表的前一个节点(X轴)prevDep: Link// 指向Link链表的下一个节点(Y轴)nextSub: Link// 指向Link链表的上一个节点(Y轴)prevSub: Link
}
前面我们讲过了新的响应式模型中Dep依赖和Sub订阅者之间不会再有直接的关联,而是通过Link作为桥梁。
那么作为桥梁的Link节点肯定需要有两个属性能够让他直接访问到Dep依赖和Sub订阅者,也就是sub和dep属性。
其中的sub属性是指向Sub订阅者,dep属性是指向Dep依赖。

我们知道Link是坐标轴的点,那这个点肯定就会有上、下、左、右四个方向。
比如对于Link1可以使用nextDep属性来访问后面这个节点Link2,Link2可以使用prevDep属性来访问前面这个节点Link1。
请注意,这里名字虽然叫nextDep和prevDep,但是他们指向的却是Link节点。然后通过这个Link节点的dep属性,就可以访问到后一个Dep依赖或者前一个Dep依赖。
同理对于Link1可以使用nextSub访问后面这个节点Link3,Link3可以使用prevSub访问前面这个节点Link1。
同样的这里名字虽然叫nextSub和prevSub,但是他们指向的却是Link节点。然后通过这个Link节点的sub属性,就可以访问到下一个Sub订阅者或者上一个Sub订阅者。
如何收集依赖
搞清楚了新的响应式模型中的三个角色:Dep依赖、Sub订阅者、Link节点,我们现在就可以开始搞清楚新的响应式模型是如何收集依赖的。
接下来我将会带你如何一步步的画出前面讲的那张新的响应式模型图。
还是我们前面的那个例子,代码如下:
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {dummy1 = counter1.value + counter2.value;console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {dummy2 = counter1.value + counter2.value + 1;console.log("dummy2", dummy2);
});counter1.value++;
counter2.value++;
</script>
大家都知道响应式变量有get和set拦截,当对变量进行读操作时会走到get拦截中,进行写操作时会走到set拦截中。
上面的例子第一个watchEffect我们叫做Sub1订阅者,第二个watchEffect叫做Sub2订阅者.
初始化时watchEffect中的回调会执行一次,这里有两个watchEffect,会依次去执行。
在Vue内部有个全局变量叫activeSub,里面存的是当前active的Sub订阅者。
执行第一个watchEffect回调时,当前的activeSub就是Sub1。
在Sub1中使用到了响应式变量counter1和counter2,所以会对这两个变量依次进行读操作。
第一个watchEffect对counter1进行读操作
先对counter1进行读操作时,会走到get拦截中。核心代码如下:
class RefImpl {
get value() {this.dep.track();return this._value;
}
}
从上面可以看到在get拦截中直接调用了dep依赖的track方法进行依赖收集。
在执行track方法之前我们思考一下当前响应式系统中有哪些角色,分别是Sub1和Sub2这两个watchEffect回调函数订阅者,以及counter1和counter2这两个Dep依赖。此时的响应式模型如下图:

从上图可以看到此时只有X坐标轴的Dep依赖,以及Y坐标轴的Sub订阅者,没有一个Link节点。
我们接着来看看dep依赖的track方法,核心代码如下:
class Dep {
// 指向Link链表的尾部节点
subs: Link;
track() {let link = new Link(activeSub, this);if (!activeSub.deps) {activeSub.deps = activeSub.depsTail = link;} else {link.prevDep = activeSub.depsTail;activeSub.depsTail!.nextDep = link;activeSub.depsTail = link;}addSub(link);
}
}
从上面的代码可以看到,每执行一次track方法,也就是说每次收集依赖,都会执行new Link去生成一个Link节点。
并且传入两个参数,activeSub为当前active的订阅者,在这里就是Sub1(第一个watchEffect)。第二个参数为this,指向当前的Dep依赖对象,也就是Dep1(counter1变量)。
先不看track后面的代码,我们来看看Link这个class的代码,核心代码如下:
class Link {
// 指向Link链表的后一个节点(X轴)
nextDep: Link;
// 指向Link链表的前一个节点(X轴)
prevDep: Link;
// 指向Link链表的下一个节点(Y轴)
nextSub: Link;
// 指向Link链表的上一个节点(Y轴)
prevSub: Link;
- constructor(public sub: Subscriber, public dep: Dep) {// ...省略
}
}
细心的小伙伴可能发现了在Link中没有声明sub和dep属性,那么为什么前面我们会说Link节点中的sub和dep属性分别指向Sub订阅者和Dep依赖呢?
因为在constructor构造函数中使用了public关键字,所以sub和dep就作为属性暴露出来了。
执行完let link = new Link(activeSub, this)后,在响应式系统模型中初始化出来第一个Link节点,如下图:

从上图可以看到Link1的sub属性指向Sub1订阅者,dep属性指向Dep1依赖。
我们接着来看track方法中剩下的代码,如下:
class Dep {
// 指向Link链表的尾部节点
subs: Link;
track() {let link = new Link(activeSub, this);if (!activeSub.deps) {activeSub.deps = activeSub.depsTail = link;} else {link.prevDep = activeSub.depsTail;activeSub.depsTail!.nextDep = link;activeSub.depsTail = link;}addSub(link);
}
}
先来看if (!activeSub.deps),activeSub前面讲过了是Sub1。activeSub.deps就是Sub1的deps属性,也就是Sub1队列上的第一个Link。
从上图中可以看到此时的Sub1并没有箭头指向Link1,所以if (!activeSub.deps)为true,代码会执行
activeSub.deps = activeSub.depsTail = link;
deps和depsTail属性分别指向Sub1队列的头部和尾部,当前队列中只有Link1这一个节点,那么头部和尾部当然都指向Link1。
执行完这行代码后响应式模型图就变成下面这样的了,如下图:

从上图中可以看到Sub1的队列中只有Link1这一个节点,所以队列的头部和尾部都指向Link1。
处理完Sub1的队列,但是Dep1的队列还没处理,Dep1的队列是由addSub(link)函数处理的。addSub函数代码如下:
function addSub(link: Link) {
const currentTail = link.dep.subs;
if (currentTail !== link) {link.prevSub = currentTail;if (currentTail) currentTail.nextSub = link;
}
link.dep.subs = link;
}
由于Dep1队列中没有Link节点,所以此时在addSub函数中主要是执行第三块代码:link.dep.subs = link。`
link.dep是指向Dep1,前面我们讲过了Dep依赖的subs属性指向队列的尾部。所以link.dep.subs = link就是将Link1指向Dep1的队列的尾部,执行完这行代码后响应式模型图就变成下面这样的了,如下图:

到这里对第一个响应式变量counter1进行读操作进行的依赖收集就完了。
第一个watchEffect对counter2进行读操作
在第一个watchEffect中接着会对counter2变量进行读操作。同样会走到get拦截中,然后执行track函数,代码如下:
class Dep {// 指向Link链表的尾部节点subs: Link;track() {let link = new Link(activeSub, this);if (!activeSub.deps) {activeSub.deps = activeSub.depsTail = link;} else {link.prevDep = activeSub.depsTail;activeSub.depsTail!.nextDep = link;activeSub.depsTail = link;}addSub(link);}
}
同样的会执行一次new Link(activeSub, this),然后把新生成的Link2的sub和dep属性分别指向Sub1和Dep2。执行后的响应式模型图如下图:

从上面的图中可以看到此时Sub1的deps属性是指向Link1的,所以这次代码会走进else模块中。else部分代码如下:
link.prevDep = activeSub.depsTail;
activeSub.depsTail.nextDep = link;
activeSub.depsTail = link;
activeSub.depsTail指向Sub1队列尾部的Link,值是Link1。所以执行link.prevDep = activeSub.depsTail就是将Link2的prevDep属性指向Link1。
同理activeSub.depsTail.nextDep = link就是将Link1的nextDep属性指向Link2,执行完这两行代码后Link1和Link2之间就建立关系了。如下图:

从上图中可以看到此时Link1和Link2之间就有箭头连接,可以互相访问到对方。
最后就是执行activeSub.depsTail = link,这行代码是将Sub1队列的尾部指向Link2。执行完这行代码后模型图如下:

Sub1订阅者的队列就处理完了,接着就是处理Dep2依赖的队列。Dep2的处理方式和Dep1是一样的,让Dep2队列的队尾指向Link2,处理完了后模型图如下:

到这里第一个watchEffect(也就是Sub1)对其依赖的两个响应式变量counter1(也就是Dep1)和counter2(也就是Dep2),进行依赖收集的过程就执行完了。
第二个watchEffect对counter1进行读操作
接着我们来看第二个watchEffect,同样的还是会对counter1进行读操作。然后触发其get拦截,接着执行track方法。回忆一下track方法的代码,如下:
class Dep {// 指向Link链表的尾部节点subs: Link;track() {let link = new Link(activeSub, this);if (!activeSub.deps) {activeSub.deps = activeSub.depsTail = link;} else {link.prevDep = activeSub.depsTail;activeSub.depsTail!.nextDep = link;activeSub.depsTail = link;}addSub(link);}
}
这里还是会使用new Link(activeSub, this)创建一个Link3节点,节点的sub和dep属性分别指向Sub2和Dep1。如下图:

同样的Sub2队列上此时还没任何值,所以if (!activeSub.deps)为true,和之前一样会去执行activeSub.deps = activeSub.depsTail = link;将Sub2队列的头部和尾部都设置为Link3。如下图:

处理完Sub2队列后就应该调用addSub函数来处理Dep1的队列了,回忆一下addSub函数,代码如下:
function addSub(link: Link) {const currentTail = link.dep.subs;if (currentTail !== link) {link.prevSub = currentTail;if (currentTail) currentTail.nextSub = link;}link.dep.subs = link;
}
link.dep指向Dep1依赖,link.dep.subs指向Dep1依赖队列的尾部。从前面的图可以看到此时队列的尾部是Link1,所以currentTail的值就是Link1。
if (currentTail !== link)也就是判断Link1和Link3是否相等,很明显不相等,就会走到if的里面去。
接着就是执行link.prevSub = currentTail,前面讲过了此时link就是Link3,currentTail就是Link1。执行这行代码就是将Link3的prevSub属性指向Link1。
接着就是执行currentTail.nextSub = link,这行代码是将Link1的nextSub指向Link3。
执行完上面这两行代码后Link1和Link3之间就建立联系了,可以通过prevSub和nextSub属性访问到对方。如下图:

接着就是执行link.dep.subs = link,将Dep1队列的尾部指向Link3,如下图:

到这里第一个响应式变量counter1进行依赖收集就完成了。
第二个watchEffect对counter2进行读操作
在第二个watchEffect中接着会对counter2变量进行读操作。同样会走到get拦截中,然后执行track函数,代码如下:
class Dep {// 指向Link链表的尾部节点subs: Link;track() {let link = new Link(activeSub, this);if (!activeSub.deps) {activeSub.deps = activeSub.depsTail = link;} else {link.prevDep = activeSub.depsTail;activeSub.depsTail!.nextDep = link;activeSub.depsTail = link;}addSub(link);}
}
这里还是会使用new Link(activeSub, this)创建一个Link4节点,节点的sub和dep属性分别指向Sub2和Dep2。如下图:

此时的activeSub就是Sub2,activeSub.deps就是指向Sub2队列的头部。所以此时头部是指向Link3,代码会走到else模块中。
在else中首先会执行link.prevDep = activeSub.depsTail,activeSub.depsTail是指向Sub2队列的尾部,也就是Link3。执行完这行代码后会将Link4的prevDep指向Link3。
接着就是执行activeSub.depsTail!.nextDep = link,前面讲过了activeSub.depsTail是指向Link3。执行完这行代码后会将Link3的nextDep属性指向Link4。
执行完上面这两行代码后Link3和Link4之间就建立联系了,可以通过nextDep和prevDep属性访问到对方。如下图:

接着就是执行activeSub.depsTail = link,将Sub2队列的尾部指向Link4。如下图:

接着就是执行addSub函数处理Dep2的队列,代码如下:
function addSub(link: Link) {const currentTail = link.dep.subs;if (currentTail !== link) {link.prevSub = currentTail;if (currentTail) currentTail.nextSub = link;}link.dep.subs = link;
}
link.dep指向Dep2依赖,link.dep.subs指向Dep2依赖队列的尾部。从前面的图可以看到此时队列的尾部是Link2,所以currentTail的值就是Link2。前面讲过了此时link就是Link4,if (currentTail !== link)也就是判断Link2和Link4是否相等,很明显不相等,就会走到if的里面去。
接着就是执行link.prevSub = currentTail,currentTail就是Link2。执行这行代码就是将Link4的prevSub属性指向Link2。
接着就是执行currentTail.nextSub = link,这行代码是将Link2的nextSub指向Link4。
执行完上面这两行代码后Link2和Link4之间就建立联系了,可以通过prevSub和nextSub属性访问到对方。如下图:

最后就是执行link.dep.subs = link将Dep2队列的尾部指向Link4,如下图:

至此整个依赖收集过程就完成了,最终就画出了Vue新的响应式模型。
依赖触发
当执行counter1.value++时,就会被变量counter1(也就是Dep1依赖)的set函数拦截。
此时就可以通过Dep1的subs属性指向队列的尾部,也就是指向Link3。
Link3中可以直接通过sub属性访问到订阅者Sub2,也就是第二个watchEffect,从而执行第二个watchEffect的回调函数。
接着就是使用Link的preSub属性从队尾依次移动到队头,从而触发Dep1队列中的所有Sub订阅者。
在这里就是使用preSub属性访问到Link1(就到队列的头部啦),Link1中可以直接通过sub属性访问到订阅者Sub1,也就是第一个watchEffect,从而执行第一个watchEffect的回调函数。
总结
这篇文章讲了Vue新的响应式模型,里面主要有三个角色:Dep依赖、Sub订阅者、Link节点。
Dep依赖和Sub订阅者不再有直接的联系,而是通过Link节点作为桥梁。
依赖收集的过程中会构建Dep依赖的队列,队列是由Link节点组成。以及构建Sub订阅者的队列,队列同样是由Link节点组成。
依赖触发时就可以通过Dep依赖的队列的队尾出发,Link节点可以访问和触发对应的Sub订阅者。
然后依次从队尾向队头移动,依次触发队列中每个Link节点的Sub订阅者。
最后推荐一下欧阳自己写的开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升,并且这本书初、中级前端能看懂。完全免费,只求一个star。
相关文章:
看不懂来打我!让性能提升56%的Vue3.5响应式重构
前言 在Vue3.5版本中最大的改动就是响应式重构,重构后性能竟然炸裂的提升了56%。之所以重构后的响应式性能提升幅度有这么大,主要还是归功于:双向链表和版本计数。这篇文章我们来讲讲使用双向链表后,Vue内部是如何实现依赖收集和…...
Halcon 极坐标变换
(1)极坐标的展开:polar_trans_image_ext(Image : PolarTransImage : Row, Column, AngleStart, AngleEnd, RadiusStart, RadiusEnd, Width, Height, Interpolation : ) (2)极坐标的逆变换:polar_trans_ima…...
JavaScript进阶--深入面向对象
深入面向对象 编程思想 面向过程:多个步骤> 解决问题 性能较高,适合跟硬件联系很紧密的东西,如单片机 但代码维护成本高,扩展性差 面向对象:问题所需功能分解为一个一个的对象(分工合作)>…...
Python列表专题:list与in
Python是一种强大的编程语言,其中列表(list)是最常用的数据结构之一。列表允许我们存储多个元素,并且可以方便地进行各种操作。在Python中,in运算符被广泛用于检测元素是否存在于列表中。本文将深入探讨Python列表及其与in运算符的结合使用。 1. Python列表的基础 1.1 什…...
利用Microsoft Entra Application Proxy在无公网IP条件下安全访问内网计算机
在现代混合办公环境中,如何让员工能够从任何地方安全访问公司内部资源成为了企业的重要挑战。传统的VPN解决方案虽然可以满足需求,但有时配置复杂,并可能涉及公网IP的问题。为了解决这个问题,Microsoft Entra(原Azure …...
【IEEE独立出版 | 厦门大学主办】第四届人工智能、机器人和通信国际会议(ICAIRC 2024)
【IEEE独立出版 | 厦门大学主办】 第四届人工智能、机器人和通信国际会议(ICAIRC 2024) 2024 4th International Conference on Artificial Intelligence, Robotics, and Communication 2024年12月27-29日 | 中国厦门 >>往届均已成功见刊检索…...
C++ 内存布局 - Part5: 继承关系中 构造析构与vptr的调整
这里以单继承为例,汇编采用AT&T格式,先看示例代码: #include <iostream>class Base { public:Base() {std::cout << "Base Constructor, this ptr: " << this << std::endl;printVptr();}virtual ~Ba…...
BUG-AttributeError: ‘EnforcedForest‘ object has no attribute ‘node‘
File “/home/adt/miniconda3/envs/bevdet/lib/python3.7/site-packages/trimesh/scene/transforms.py”, line 224, in nodes_geometry ‘geometry’ in self.transforms.node[n]): AttributeError: ‘EnforcedForest’ object has no attribute ‘node’ networkx 2.6.3 pyp…...
Spring Boot 3 配置 Redis 兼容单例和集群
配置项 Spring Boot 3.x 的 redis 配置和 Spring Boot 2.x 是不一样的, 路径多了一个data spring:...data:redis:host: redis.hostport: redis.portpassword: redis.passworddatabase: redis.database兼容单例和集群的配置 开发时一般用一个Redis单例就足够, 测试和生产环境…...
unsat钱包签名算法解析
unsat钱包签名算法解析 在数字货币领域,安全性是至关重要的,而签名算法则是确保交易和信息不可伪造的基础。本文将深入解析 unsat 钱包中使用的签名算法,重点关注如何生成和验证消息签名。 1. 签名算法概述 unsat 钱包使用 ECDSAÿ…...
mysql删除唯一索引
推荐学习文档 golang应用级os框架,欢迎stargolang应用级os框架使用案例,欢迎star案例:基于golang开发的一款超有个性的旅游计划app经历golang实战大纲golang优秀开发常用开源库汇总想学习更多golang知识,这里有免费的golang学习笔…...
学习之面试题:偏函数
偏函数(Partial Function)是 Python 中的一个实用工具,通常用于函数式编程中,可以固定一个函数的部分参数,从而生成一个新的函数。偏函数在 Python 中通常通过 functools.partial 实现。在面试中,考察偏函数…...
面试技术点
Java 一、jvm模块 jvm是什么? 是一用用于计算设备的规范,虚构出来的计算机,在计算机上仿真模拟各种计算机功能来实现 jvm 作用是什么? java中所有类必须装载jvm中才能运行,这个装载工作有jvm装载器完成,.class类型文件能在jvm虚拟器中运行,但不能直接在系统中运行,需要…...
基础sql
在执行删除操作之前,建议先运行一个 SELECT 查询来确认你要删除的记录。这可以帮助你避免误删数据。 删除字段id默认值为空字符串的所有数据 delete from users where id ; 删除字段id默认值为null的所有数据 delete from users where id is null; 删除字段upd…...
Jenkins整合Docker实现CICD自动化部署(若依项目)
前期准备 提前准备好jenkins环境 并且jenkins能使用docker命令,并且已经配置好了jdk、node、maven环境,我之前写了安装jenkins的博客,里面讲得比较详细,推荐用我这种方式安装 docker安装jenkins,并配置jdk、node和m…...
kali chrome 安装 hackbar
HackBar 是一个用于在 Kali Linux 中快速测试 SQL 注入和 XSS 漏洞的 Chrome 扩展程序。以下是如何在 Kali Linux 上安装 HackBar 的步骤: 首先,你需要确保你的系统已经安装了 Google Chrome 或 Chromium。如果没有安装,你可以使用以下命令安…...
一文了解 Linux 系统的文件权限管理
文章目录 引入Linux文件权限模型查看文件权限权限信息解析修改文件权限符号模式八进制数字模式 引入 在Linux操作系统中,我们想查看我们对文件拥有哪些权限时,可以在终端键入ls -l或ll命令,终端会输出当前路径下的文件信息,如文件…...
Spark:DataFrame介绍及使用
1. DataFrame详解 DataFrame是基于RDD进行封装的结构化数据类型,增加了schema元数据,最终DataFrame类型在计算时,还是转为rdd计算。DataFrame的结构化数据有Row(行数据)和schema元数据构成。 Row 类型 表示一行数据 …...
Linux系统:本机(物理主机)访问不了虚拟机中的apache服务问题的解决方案
学习目标: 提示:本文主要讲述-本机(物理主机)访问不了虚拟机中的apache服务情况下的解决方案 Linux系统:Ubuntu 23.04; 文中提到的“本机”:代表,宿主机,物理主机; 首先,…...
望繁信科技成功签约国显科技 流程挖掘助力制造业智造未来
近日,上海望繁信科技有限公司(简称“望繁信科技”)成功与深圳市国显科技有限公司(简称“国显科技”)达成合作。国显科技作为全球领先的TFT-LCD液晶显示及Mini/Micro LED显示产品供应商,致力于为笔记本、手机…...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...
Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...
