- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
Vue3.5响应式重构主要分为两部分:双向链表和版本计数。在上一篇文章中我们讲了 双向链表 ,这篇文章我们接着来讲版本计数.
欧阳年底也要毕业了,加入欧阳的面试交流群(分享内推信息)、高质量vue源码交流群 。
看这篇文章之前最好先看一下欧阳之前写的 双向链表 文章,不然有些部分可能看着比较迷茫.
在上篇 双向链表 文章中我们知道了新的响应式模型中主要分为三个部分:Sub订阅者、Dep依赖、Link节点.
Sub订阅者:主要有watchEffect、watch、render函数、computed等.
Dep依赖:主要有ref、reactive、computed等响应式变量.
Link节点:连接Sub订阅者和Dep依赖之间的桥梁,Sub订阅者想访问Dep依赖只能通过Link节点,同样Dep依赖想访问Sub订阅者也只能通过Link节点.
细心的小伙伴可能发现了computed计算属性不仅是Sub订阅者还是Dep依赖。 原因是computed可以像watchEffect那样监听里面的响应式变量,当响应式变量改变后会触发computed的回调.
还可以将computed的返回值当做ref那样的普通响应式变量去使用,所以我们才说computed不仅是Sub订阅者还是Dep依赖.
版本计数中由4个version实现,分别是:全局变量globalVersion、dep.version、link.version和computed.globalVersion.
globalVersion是一个全局变量,初始值为0,仅有响应式变量改变后才会触发globalVersion++.
dep.version是在dep依赖上面的一个属性,初始值是0。当dep依赖是ref这种普通响应式变量,仅有响应式变量改变后才会触发dep.version++。当computed计算属性作为dep依赖时,只有等computed最终计算出来的值改变后才会触发dep.version++.
link.version是Link节点上面的一个属性,初始值是0。每次响应式更新完了后都会保持和dep.version的值相同。在响应式更新前就是通过link.version和dep.version的值是否相同判断是否需要更新.
computed.globalVersion:计算属性上面的版本,如果computed.globalVersion === globalVersion说明没有响应式变量改变,计算属性的回调就不需要重新执行.
而版本计数最大的受益者就是computed计算属性,这篇文章接下来我们将以computed举例讲解.
我们来看个简单的demo,代码如下:
<template>
<p>{{ doubleCount }}</p>
<button @click="flag = !flag">切换flag</button>
<button @click="count1++">count1++</button>
<button @click="count2++">count2++</button>
</template>
<script setup>
import { computed, ref } from "vue";
const count1 = ref(1);
const count2 = ref(10);
const flag = ref(true);
const doubleCount = computed(() => {
console.log("computed");
if (flag.value) {
return count1.value * 2;
} else {
return count2.value * 2;
}
});
</script>
在computed中根据flag.value的值去决定到底返回count1.value * 2还是count2.value * 2.
那么问题来了,当flag的值为true时,点击count2++按钮,console.log("computed")会执行打印吗?也就是doubleCount的值会重新计算吗?
答案是:不会。虽然count2也是computed中使用到的响应式变量,但是他不参与返回值的计算,所以改变他不会导致computed重新计算.
有的同学想问为什么能够做到这么精细的控制呢?这就要归功于版本计数了,我们接下来会细讲.
还是前面那个demo,初始化时flag的值是true,所以在computed中会对count1变量进行读操作,然后触发get拦截。count1这个ref响应式变量就是由RefImpl类new出来的一个对象,代码如下:
class RefImpl {
dep: Dep = new Dep();
get value() {
this.dep.track()
}
set value() {
this.dep.trigger();
}
}
在get拦截中会执行this.dep.track(),其中dep是由Dep类new出来的对象,代码如下 。
class Dep {
version = 0;
track() {
let link = new Link(activeSub, this);
// ...省略
}
trigger() {
this.version++;
globalVersion++;
this.notify();
}
}
在track方法中使用Link类new出来一个link对象,Link类代码如下:
class Link {
version: number
/**
* Pointers for doubly-linked lists
*/
nextDep?: Link
prevDep?: Link
nextSub?: Link
prevSub?: Link
prevActiveLink?: Link
constructor(
public sub: Subscriber,
public dep: Dep,
) {
this.version = dep.version
this.nextDep =
this.prevDep =
this.nextSub =
this.prevSub =
this.prevActiveLink =
undefined
}
}
这里我们只关注Link中的version属性,其他的属性在上一篇双向链表文章中已经讲过了.
在constructor中使用dep.version给link.version赋值,保证dep.version和link.version的值是相等的,也就是等于0。因为dep.version的初始值是0,接着就会讲.
当我们点击count1++按钮时会让响应式变量count1的值自增。因为count1是一个ref响应式变量,所以会触发其set拦截。代码如下:
class RefImpl {
dep: Dep = new Dep();
get value() {
this.dep.track()
}
set value() {
this.dep.trigger();
}
}
在set拦截中执行的是this.dep.trigger(),trigger函数代码如下:
class Dep {
version = 0;
track() {
let link = new Link(activeSub, this);
// ...省略
}
trigger() {
this.version++;
globalVersion++;
this.notify();
}
}
前面讲过了globalVersion是一个全局变量,初始值为0.
dep上面的version属性初始值也是0.
在trigger中分别执行了this.version++和globalVersion++,这里的this就是指向的dep。执行完后dep.version和globalVersion的值就是1了。而此时link.version的值依然还是0,这个时候dep.version和link.version的值就已经不相等了.
接着就是执行notify方法按照新的响应式模型进行通知订阅者进行更新,我们这个例子此时新的响应式模型如下图:
如果修改的响应式变量会触发多个订阅者,比如count1变量被多个watchEffect使用,修改count1变量的值就需要触发多个订阅者的更新。notify方法中正是将多个更新操作放到一个批次中处理,从而提高性能。由于篇幅有限我们就不去细讲notify方法的内容,你只需要知道执行notify方法就会触发订阅者的更新.
(这两段是notify方法内的逻辑)按照正常的逻辑如果count1变量的值改变,就可以通过Link2节点找到Sub1订阅者,然后执行订阅者的notify方法从而进行更新.
如果我们的Sub1订阅者是render函数,是这个正常的逻辑。但是此时我们的Sub1订阅者是计算属性doubleCount,这里会有一个优化,如果订阅者是一个计算属性,触发其更新时不会直接执行计算属性的回调函数,而是直接去通知计算属性的订阅者去更新,在更新前才会去执行计算属性的回调函数(这个接下来的文章会讲)。代码如下:
if (link.sub.notify()) {
// if notify() returns `true`, this is a computed. Also call notify
// on its dep - it's called here instead of inside computed's notify
// in order to reduce call stack depth.
link.sub.dep.notify()
}
link.sub.notify()的执行结果是true就代表当前的订阅者是计算属性,然后就会触发计算属性“作为依赖”时对应的订阅者。我们这里的计算属性doubleCount是在template中使用,所以计算属性doubleCount的订阅者就是render函数.
所以这里就是调用link.sub.notify()不会触发计算属性doubleCount中的回调函数重新执行,而是去触发计算属性doubleCount的订阅者,也就是render函数。在执行render函数之前会再去通过脏检查(依靠版本计数实现)去判断是否需要重新执行计算属性的回调,如果需要执行计算属性的回调那么就去执行render函数重新渲染.
所有的Sub订阅者内部都是基于ReactiveEffect类去实现的,调用订阅者的notify方法通知更新实际底层就是在调用ReactiveEffect类中的runIfDirty方法。代码如下:
class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
/**
* @internal
*/
runIfDirty(): void {
if (isDirty(this)) {
this.run();
}
}
}
在runIfDirty方法中首先会调用isDirty方法判断当前是否需要更新,如果返回true,那么就执行run方法去执行Sub订阅者的回调函数进行更新。如果是computed、watch、watchEffect等订阅者调用run方法就会执行其回调函数,如果是render函数这种订阅者调用run方法就会再次执行render函数.
调用isDirty方法时传入的是this,值得注意的是this是指向ReactiveEffect实例。而ReactiveEffect又是继承自Subscriber订阅者,所以这里的this是指向的是订阅者.
前面我们讲过了,修改响应式变量count1的值时会通知作为订阅者的doubleCount计算属性。当通知作为订阅者的计算属性更新时不会去像watchEffect这样的订阅者一样去执行其回调,而是去通知计算属性作为Dep依赖时订阅他的订阅者进行更新。在这里计算属性doubleCount是在template中使用,所以他的订阅者是render函数.
所以修改count1变量执行runIfDirty时此时触发的订阅者是作为Sub订阅者的render函数,也就是说此时的this是render函数!! 。
我们来看看isDirty是如何进行脏检查,代码如下:
function isDirty(sub: Subscriber): boolean {
for (let link = sub.deps; link; link = link.nextDep) {
if (
link.dep.version !== link.version ||
(link.dep.computed &&
(refreshComputed(link.dep.computed) ||
link.dep.version !== link.version))
) {
return true;
}
}
return false;
}
这里就涉及到我们上一节讲过的双向链表了,回顾一下前面讲过的响应式模型图,如下图: 此时的sub订阅者是render函数,也就是图中的Sub2。sub.deps是指向指向Sub2订阅者X轴(横向)上面的Link节点组成的队列的头部,link.nextDep就是指向X轴上面下一个Link节点,通过Link节点就可以访问到对应的Dep依赖.
在这里render函数对应的订阅者Sub2在X轴上面只有一个节点Link3.
这里的for循环就是去便利Sub订阅者在X轴上面的所有Link节点,然后在for循环内部去通过Link节点访问到对应的Dep依赖去做版本计数的判断.
这里的for循环内部的if语句判断主要分为两部分:
if (
link.dep.version !== link.version ||
(link.dep.computed &&
(refreshComputed(link.dep.computed) ||
link.dep.version !== link.version))
) {
return true;
}
这两部分中只要有一个是true,那么就说明当前Sub订阅者需要更新,也就是执行其回调.
我们来看看第一个判断:
link.dep.version !== link.version
还记得我们前面讲过吗,初始化时会保持dep.version和link.version的值相同。每次响应式变量改变时走到set拦截中,在拦截中会去执行dep.version++,执行完了后此时dep.version和link.version的值就已经不相同了,在这里就能知道此时响应式变量改变过了,需要通知Sub订阅者更新执行其回调.
常规情况下Dep依赖是一个ref变量、Sub订阅者是wachEffect这种确实第一个判断就可以满足了.
但是我们这里的link.dep是计算属性doubleCount,计算属性是由ComputedRefImpl类new出来的对象,简化后代码如下:
class ComputedRefImpl<T = any> implements Subscriber {
_value: any = undefined;
readonly dep: Dep = new Dep(this);
globalVersion: number = globalVersion - 1;
get value(): T {
// ...省略
}
set value(newValue) {
// ...省略
}
}
ComputedRefImpl继承了Subscriber类,所以说他是一个订阅者。同时还有get和set拦截,以及初始化一个计算属性时也会去new一个对应的Dep依赖.
还有一点值得注意的是计算属性上面的computed.globalVersion属性初始值为globalVersion - 1,默认是不等于globalVersion的,这是为了第一次执行计算属性时能够去触发执行计算属性的回调,这个在后面的refreshComputed函数中会讲.
我们是直接修改的count1变量,在count1变量的set拦截中触发了dep.version++,但是并没有修改计算属性对应的dep.version。所以当计算属性作为依赖时单纯的使用link.dep.version !== link.version 就不能满足需求了,需要使用到第二个判断:
(link.dep.computed &&
(refreshComputed(link.dep.computed) ||
link.dep.version !== link.version))
在第二个判断中首先判断当前当前的Dep依赖是不是计算属性,如果是就调用refreshComputed函数去执行计算属性的回调。然后判断计算属性的结果是否改变,如果改变了在refreshComputed函数中就会去执行link.dep.version++,所以执行完refreshComputed函数后link.dep.version和link.version的值就不相同了,表示计算属性的值更新了,当然就需要执行依赖计算属性的render函数啦.
我们来看看refreshComputed函数的代码,简化后的代码如下:
function refreshComputed(computed: ComputedRefImpl): undefined {
if (computed.globalVersion === globalVersion) {
return;
}
computed.globalVersion = globalVersion;
const dep = computed.dep;
try {
prepareDeps(computed);
const value = computed.fn(computed._value);
if (dep.version === 0 || hasChanged(value, computed._value)) {
computed._value = value;
dep.version++;
}
} catch (err) {
dep.version++;
throw err;
} finally {
cleanupDeps(computed);
}
}
首先会去判断computed.globalVersion === globalVersion是否相等,如果相等就说明根本就没有响应式变量改变,那么当然就无需去重新执行计算属性回调.
还记得我们前面讲过每当响应式变量改变后触发set拦截是都会执行globalVersion++吗?所以这里就可以通过computed.globalVersion === globalVersion判断是否有响应式变量改变,如果没有说明计算属性的值肯定就没有改变.
接着就是执行computed.globalVersion = globalVersion将computed.globalVersion的值同步为globalVersion,为了下次判断是否需要重新执行计算属性做准备.
在try中会先去执行prepareDeps函数,这个先放放接下来讲,先来看看try中其他的代码.
首先调用const value = computed.fn(computed._value)去重新执行计算属性的回调函数拿到计算属性新的返回值value.
接着就是执行if (dep.version === 0 || hasChanged(value, computed._value)) 。
我们前面讲过了dep上面的version默认值为0,这里的dep.version === 0说明是第一次渲染计算属性。接着就是使用hasChanged(value, computed._value)判断计算属性新的值和旧的值相比较是否有修改.
上面这两个条件满足一个就执行if里面的内容,将新得到的计算属性的值更新上去,并且执行dep.version++。因为前面讲过了在外面会使用link.dep.version !== link.version判断dep的版本是否和link上面的版本是否相同,如果不相等就执行render函数.
这里由于计算属性的值确实改变了,所以会执行dep.version++,dep的版本和link上面的版本此时就不同了,所以就会被标记为dirty,从而执行render函数.
如果执行计算属性的回调函数出错了,同样也执行一次dep.version++.
最后就是剩余执行计算属性回调函数之前调用的prepareDeps和finally调用的cleanupDeps函数没讲了.
回顾一下demo的代码:
<template>
<p>{{ doubleCount }}</p>
<button @click="flag = !flag">切换flag</button>
<button @click="count1++">count1++</button>
<button @click="count2++">count2++</button>
</template>
<script setup>
import { computed, ref } from "vue";
const count1 = ref(1);
const count2 = ref(10);
const flag = ref(true);
const doubleCount = computed(() => {
console.log("computed");
if (flag.value) {
return count1.value * 2;
} else {
return count2.value * 2;
}
});
</script>
当flag的值为true时,对应的响应式模型前面我们已经讲过了,如下图:
如果我们将flag的值设置为false呢?此时的计算属性doubleCount就不再依赖于响应式变量count1,而是依赖于响应式变量count2。小伙伴们猜猜此时的响应式模型应该是什么样的呢?
现在多了一个count2变量对应的Link4,原本Link1和Link2之间的连接也因为计算属性不再依赖于count1变量后,他们俩之间的连接也没有了,转而变成了Link1和Link4之间建立连接.
前面没有讲的prepareDeps和cleanupDeps函数就是去掉Link1和Link2之间的连接.
prepareDeps函数代码如下:
function prepareDeps(sub: Subscriber) {
// Prepare deps for tracking, starting from the head
for (let link = sub.deps; link; link = link.nextDep) {
// set all previous deps' (if any) version to -1 so that we can track
// which ones are unused after the run
link.version = -1
// store previous active sub if link was being used in another context
link.prevActiveLink = link.dep.activeLink
link.dep.activeLink = link
}
}
这里使用for循环遍历计算属性Sub1在X轴上面的Link节点,也就是Link1和Link2,并且将这些Link节点的version属性设置为-1.
当flag的值设置为false后,重新执行计算属性doubleCount中的回调函数时,就会对回调函数中的所有响应式变量进行读操作。从而再次触发响应式变量的get拦截,然后执行track方法进行依赖收集。注意此时新收集了一个响应式变量count2。收集完成后响应式模型图如下图:
从上图中可以看到虽然计算属性虽然不再依赖count1变量,但是count1变量变量对应的Link2节点还在队列的连接上.
我们在prepareDeps方法中将计算属性依赖的所有Link节点的version属性都设置为-1,在track方法收集依赖时会执行这样一行代码,如下:
class Dep {
track() {
if (link === undefined || link.sub !== activeSub) {
// ...省略
} else if (link.version === -1) {
link.version = this.version;
// ...省略
}
}
}
如果link.version === -1,那么就将link.version的值同步为dep.version的值.
只有计算属性最新依赖的响应式变量才会触发track方法进行依赖收集,从而将对应的link.version从-1更新为dep.version.
而变量count1现在已经不会触发track方法了,所以变量count1对应的link.version的值还是-1.
最后就是执行cleanupDeps函数将link.version的值还是-1的响应式变量(也就是不再使用的count1变量)对应的Link节点,从双向链表中给干掉。代码如下:
function cleanupDeps(sub: Subscriber) {
// Cleanup unsued deps
let head;
let tail = sub.depsTail;
let link = tail;
while (link) {
const prev = link.prevDep;
if (link.version === -1) {
if (link === tail) tail = prev;
// unused - remove it from the dep's subscribing effect list
removeSub(link);
// also remove it from this effect's dep list
removeDep(link);
} else {
// The new head is the last node seen which wasn't removed
// from the doubly-linked list
head = link;
}
// restore previous active link if any
link.dep.activeLink = link.prevActiveLink;
link.prevActiveLink = undefined;
link = prev;
}
// set the new head & tail
sub.deps = head;
sub.depsTail = tail;
}
遍历Sub1计算属性横向队列(X轴)上面的Link节点,当link.version === -1时,说明这个Link节点对应的Dep依赖已经不被计算属性所依赖了,所以执行removeSub和removeDep将其从双向链表中移除.
执行完cleanupDeps函数后此时的响应式模型就是我们前面所提到的样子,如下图:
版本计数主要有四个版本:全局变量globalVersion、dep.version、link.version和computed.globalVersion。dep.version和link.version如果不相等就说明当前响应式变量的值改变了,就需要让Sub订阅者进行更新.
如果是计算属性作为Dep依赖时就不能通过dep.version和link.version去判断了,而是执行refreshComputed函数进行判断。在refreshComputed函数中首先会判断globalVersion和computed.globalVersion是否相等,如果相等就说明并没有响应式变量更新。如果不相等那么就会执行计算属性的回调函数,拿到最新的值后去比较计算属性的值是否改变。并且还会执行prepareDeps和cleanupDeps函数将那些计算属性不再依赖的响应式变量对应的Link节点从双向链表中移除.
最后说一句,版本计数最大的赢家应该是computed计算属性,虽然引入版本计数后代码更难理解了。但是整体流程更加优雅,以及现在只需要通过判断几个version是否相等就能知道订阅者是否需要更新,性能当然也更好了.
关注公众号:【前端欧阳】,给自己一个进阶vue的机会 。
另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star.
最后此篇关于让性能提升56%的Vue3.5响应式重构之“版本计数”的文章就讲到这里了,如果你想了解更多关于让性能提升56%的Vue3.5响应式重构之“版本计数”的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
在本教程中,您将通过示例学习 JavaScript。 JavaScript 中的提升是一种在声明之前可以使用函数或变量的行为。例如, // using test before declarin
我正在学习javascript提升功能,发现下面的代码真的很困惑: var a = 1; function b() { a = 10; return; function a()
作为一个JS学习者,我发现了一件很有趣的事情,考虑下面的代码。 this.init = function (e) { var container = e.container;
Quasiquotes 的 Scala 文档在解释 Lifting 时提到了这一点: One can also combine lifting and unquote splicing: scala
我是新来的。到目前为止,我一直在使用 MVC 模型并使用基本的 session 管理模型,即在 session 中存储一个 token 并检查每个请求。 我正在尝试对lift做同样的事情,但我的 se
我当前使用的是Elasticsearch 2.4版,希望根据查询时间的增加或加权,根据我称为“类型”的字段对结果集进行微调。 例如 如果字段“类型”的值为“船”,则将权重或增强值增加4 如果字段“类型
一年多以来,我一直在大量使用 lift、return 以及 EitherT、ReaderT< 等构造函数,等等。我读过《Real World Haskell》、《Learn You a Haskell
我浏览了电梯的MegaProtoUser遇到这种结构:??("Last Name")。谁能解释一下,这是什么意思? 谢谢解答 最佳答案 它是在对象 S 上定义的: def ??(str : Strin
我有一个Solr索引,每个文档都是一个Event的信息。在我的架构中,Schedule 是日期类型的多值字段。我想知道是否可以使用计划日期来增加文档(多值字段中的任何日期)在未来并且最接近当前日期?我
作为测试,我正在尝试使用设计人员友好的模板在 lift 中创建一个表单。我正在使用 Lift 2.5 我已经设法使用 toForm 创建了一个工作表单,但我只是在探索所有可能的方法。 我的 html
如果这个问题已经被问到,我深表歉意。 是否可以清除已经设置的条件变量? 下面是我想要实现的详细信息: void worker_thread { while (wait_for_conditio
尝试学习Js,无法理解为什么DOM元素没有获取到值: var Car = function(loc) { var obj = Object.create(Car.prototype); obj
我想知道吊装。我知道如果全局函数名称与全局变量相同,函数会覆盖变量的名称。是吗? 这是我的代码。 (function() { console.log('console.log#1 ' + glob
这个问题已经有答案了: var functionName = function() {} vs function functionName() {} (41 个回答) 已关闭 7 年前。 在javas
我正在开发 Windows 资源管理器 namespace 扩展。我的应用程序是explorer.exe在某个时候加载和使用的动态库。我需要我的 DLL 在 C:\中创建文件,有时在其他需要提升才能执
背景: GitHub 属于客户。我们团队中有一些新手,他们有时会错过基本的命名约定和其他编码协议(protocol)。所以,如果哪位前辈想在内部review,除了创建PR,别无他法。但是这个 PR 对
我们需要在运行时更改 HKEY_LOCAL_MACHINE 的一些设置。 如果需要在运行时,是否可以提示 uac 提升,或者我是否必须启动第二个提升的进程来完成“肮脏的工作”? 最佳答案 我会以提升的
看着Haskell文档,提升似乎基本上是 fmap 的概括,允许映射具有多个参数的函数。 Wikipedia然而,关于提升的文章给出了不同的观点,根据类别中的态射来定义“提升”,以及它如何与类别中的其
ggplot2 package 很容易成为我用过的最好的绘图系统,除了对于较大的数据集(约 50k 点)性能不是很好。我正在研究通过 Shiny 提供网络分析,使用 ggplot2作为绘图后端,但我对
是否可以提升 powershell 脚本的权限,以便没有管理员权限的用户可以运行该脚本?我们的网络管理员正在尝试寻找更省时的方法来完成某些任务,目前他们必须使用远程桌面...使用 PS 脚本将其自动化
我是一名优秀的程序员,十分优秀!