企业站系统,网店代运营被骗怎么办,想学网站建设什么的,手机网站表单页面制作前言
在Vue3.5版本中最大的改动就是响应式重构#xff0c;重构后性能竟然炸裂的提升了56%。之所以重构后的响应式性能提升幅度有这么大#xff0c;主要还是归功于#xff1a;双向链表和版本计数。这篇文章我们来讲讲使用双向链表后#xff0c;Vue内部是如何实现依赖收集和…前言
在Vue3.5版本中最大的改动就是响应式重构重构后性能竟然炸裂的提升了56%。之所以重构后的响应式性能提升幅度有这么大主要还是归功于双向链表和版本计数。这篇文章我们来讲讲使用双向链表后Vue内部是如何实现依赖收集和依赖触发的。搞懂了这个之后你就能掌握Vue3.5重构后的响应式原理至于版本计数如果大家感兴趣可以在评论区留言关注的人多了欧阳后面会再写一篇版本计数的文章。
加入欧阳的高质量vue源码交流群、欧阳平时写文章参考的多本vue源码电子书
3.5版本以前的响应式
在Vue3.5以前的响应式中主要有两个角色Sub订阅者、Dep依赖。其中的订阅者有watchEffect、watch、render函数、computed等。依赖有ref、reactive等响应式变量。
举个例子
script setup langts
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 langts
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依赖对应的就是变量counter1Dep2依赖对应的就是变量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就组成一个队列。其中的队尾就是Link2depsTail属性队头就是Link1deps属性。
还有就是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属性来访问后面这个节点Link2Link2可以使用prevDep属性来访问前面这个节点Link1。
请注意这里名字虽然叫nextDep和prevDep但是他们指向的却是Link节点。然后通过这个Link节点的dep属性就可以访问到后一个Dep依赖或者前一个Dep依赖。
同理对于Link1可以使用nextSub访问后面这个节点Link3Link3可以使用prevSub访问前面这个节点Link1。
同样的这里名字虽然叫nextSub和prevSub但是他们指向的却是Link节点。然后通过这个Link节点的sub属性就可以访问到下一个Sub订阅者或者上一个Sub订阅者。
如何收集依赖
搞清楚了新的响应式模型中的三个角色Dep依赖、Sub订阅者、Link节点我们现在就可以开始搞清楚新的响应式模型是如何收集依赖的。
接下来我将会带你如何一步步的画出前面讲的那张新的响应式模型图。
还是我们前面的那个例子代码如下
script setup langts
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依赖对象也就是Dep1counter1变量。
先不看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就是Link3currentTail就是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就是Sub2activeSub.deps就是指向Sub2队列的头部。所以此时头部是指向Link3代码会走到else模块中。
在else中首先会执行link.prevDep activeSub.depsTailactiveSub.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就是Link4if (currentTail ! link)也就是判断Link2和Link4是否相等很明显不相等就会走到if的里面去。
接着就是执行link.prevSub currentTailcurrentTail就是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。