当前位置: 首页 > news >正文

Vue.js 学习总结(13)—— Vue3 version 计数介绍

前言

Vue3.5 提出了两个重要概念:version计数和双向链表,作为在内存和计算方面性能提升的最大功臣。既然都重要,那就单挑 version 计数来介绍,它在依赖追踪过程中,起到快速判断依赖项有没有更新的作用,所以当通过computed(fn)读取值时,fn变的更聪明了,要是version没增加,那我就懒得执行。什么是懒更新(lazy update)?先看一个示例:

const a = ref(0)
const b = ref(0)
const check = ref(true)// step 1
const c = computed(() => {console.log('computed')if (check.value) {return a.value} else {return b.value}
}) // step 2
a.value++effect(() => {// step 3console.log('effect')c.valuec.value
})
// step 4
b.value++
// step 5
check.value= false
// step 6
b.value++

以上代码打印的结果是什么?

effect
computed
computed
effect
computed
effect

为什么是这样的结果?

  • step 1:在调用computed函数时,fn不会立即执行,仅读取c时才会执行;

  • step 2: 由于c的fn未执行,a没有订阅者,没有日志打印;

  • setp 3: effect会立即执行,先打印 effect ,第一次读取c.value时打印 computed,第二次读取c.value时其结果没变化,因此c的fn不执行;

  • step 4: 虽然b.value有更新,但此时c的依赖项仅acheck,因此c的fn不执行;

  • step 5: check.value有更新,c的fn执行,打印 computed,此时c的依赖项变为checkbc更新,则effect也重新执行,打印 effect

  • step 6: 逻辑和step 5一致,因此打印日志 computedeffect

什么是懒更新? 像computed(fn)effect(fn)这类函数,仅但其计算结果被使用,或者fn中包含的响应式依赖项有更新时,才会触发fn的重新执行, 减少了不必要的计算。那么 vue3.5 提出的 version counting 在懒更新过程中起到什么作用,以及如何起作用的? vue 源码中一共有三个地方涉及到vesion计数:全局的globalVersion、Dep对象的version字段、双向链表包含的version字段,接下来我们就通过triggertrack过程将version串联起来讲解。

globalVersion

提出globalVersion的人真是个天才,我了解最早是在preact设计高效响应式框架@preact/signals-core时提出的想法,通过一个全局的version计数器快速判断是否有reactive发生变更,否则中断更新。globalVersion的定义如下,原则是只要reactive对象有任何更新,则其值自增

// file: dep.ts/**
* reactive有更新,则globalVersion++
* 为computed提供快速判断是否需要re-compute
*/
export let globalVersion = 0

vue有且仅有一个入口会触发globalVersion的自增,这个入口就是trigger函数,当任何Ref、Reactive等响应式对象值更新,都会触发trigger函数。所以trigger函数的作用就是根据target找到依赖项集合deps,并通知这些依赖项target值更新了。

export function trigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,...
): void {const depsMap = targetMap.get(target)...
}

上述代码为trigger函数签名,函数逻辑有些复杂,我们只需要聚焦到和globalVersion相关的逻辑。函数第一行通过targetMap获取target的依赖项集合,targgetMap的格式为{target -> key -> dep}, 因此depsMap的格式为key -> dep

if (!depsMap) {// never been trackedglobalVersion++return
}

如果depsMap为空,表明当前taget没有依赖项,target值有更新,globalVesion需要自增,但由于没有依赖项,所以直接结束trigger流程。当depsMap不为空,则接着往下走。

let deps: Dep[] = []
...

接下来很长一段逻辑都是收集target.key对应的依赖项,并将其放到deps集合中, 当拿到deps集合后, 就可以为每个dep触发更新了。接下来的代码很重要:

startBatch()
for (const dep of deps) {dep.trigger()
}
endBatch()

vue3.5引入了批处理APIstartBatchendBatch,其作用是使两者中间的所有依赖更新effect暂时不触发,并将所有effect使用链表关联起来,那什么时候触发依赖更新?答案是,在endBatch中遍历链表批量更新,这样处理有什么好处?我们稍后再回答

export function startBatch(): void {batchDepth++
}

startBatch中执行batchDepth++,在startBatchendBatch中间的依赖更新会判断其是否为0,不为0则跳过更新流程。接下来遍历deps,并调用dep.trigger函数通知更新。

// file: effect.tstrigger(): void {this.version++globalVersion++this.notify()
}

当触发dep.trigger函数,globalVersion自增,dep的version也会自增,后续sub(订阅者)获取值时,使用sub.version和dep.version判断就能快速确认是否要更新。最后一行调用notify执行通知。

  notify(): void {startBatch()try {for (let link = this.subs; link; link = link.prevSub) {link.sub.notify()}} finally {endBatch()}}

startBatchendBatch又成对出现了,作用一样,让其中间的更新流程暂不执行,先挂在全局的batchedEffect链表上,在合适的时机(batchDepth为0)再遍历链表统一处理。唉,还是绕不开链表,但结合代码更容易理解。结合下图分析,假如target对应的dep为Dep1,上述代码的try代码块会从链表head(this.subs)开始遍历subs链表节点,对应下图则就是从上往下的链式结构link1 -> link2 -> link3,而根据link节点又能找到对应的Sub(订阅者),是不是很方便?

图片

link.sub.notify()具体执行了什么?

// file:effect.tsnotify(): void {...this.nextEffect = batchedEffectbatchedEffect = this
}

subscriber的notify这么简单的吗?没错,它的作用就是将当前effect挂到上文提到的批更新处理的batchedEffect链表head上,那真正的更新在哪里处理?当然是endBatch了。还是以上述的链表图为例,此时batchedEffect指向了link3对应的Sub3, 而batchedEffect链表的顺序为Sub3Sub2Sub1

export function endBatch(): void {if (--batchDepth > 0) {return}while (batchedEffect) {let e: ReactiveEffect | undefined = batchedEffectbatchedEffect = undefinedwhile (e) {const next: ReactiveEffect | undefined = e.nextEffecte.nextEffect = undefinede.trigger()e = next}}
}

上述代码为endBatch函数的简化版,首先判断batchDepth是否大于0,满足则结束,当且仅当batchedEffect包含完整link链路(batchDepth为0)时,才执行while逻辑。while代码块的逻辑大家应该熟悉,不就是遍历链表的节点吗?并且每遍历一个就执行sub的trigger函数,直到链表遍历结束。

// file: effect.ts// trigger函数简化版
trigger(): void {if (isDirty(this)) {this.run()}
}

trigger函数的逻辑是,先调用isDirty(this)看下我是否脏了,脏了就得重新run, 计算最新值。到目前,从target触发的更新,经过globalVersion、dep.version的自增标记了更新,并且经过一系列链路也通知到订阅者(Sub), 接下来就该看订阅者的表演。思考:trigger函数中isDirty用于判断是否需要重新计算,那isDirty是不是会用到globalVersion或dep.version?

version 计数的作用

趁热打铁,还是从上节的trigger函数说起,先调用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) === false)) {return true}}return false
}

唉,又是绕不开链表!但说曹操,曹操就到,这不就call back了上节最后提的思考,果真有使用version。假设isDirty(sub)中的sub是下图的Sub3,那么其deps节点,指向的是link3,而整个链表完整的节点顺序为link3->link6->link 9

图片

在遍历每个节点link时,需要做version比对,if语句由两部分组成,判断前先说明几个字段:

  • link.dep:dep为上文target触发更新关联的deps集合中的一项,可以理解为和link是一一对应;

  • link.dep.computed:如果订阅者Sub的依赖项是computed结构,那么link.dep.computed即是类型为ComputedRefImpl的实体对象。

  • refreshComputed:用于判断computed是否需要重新计算,需要则调用computed(fn)中的fn计算最新结果。

接下来继续分析if判断逻辑:

判断1:link.dep.version !== link.version 经过上一节target触发更新,dep.version已经自增,而link.version还为上一个版本,因此Sub需要重新计算。假设有如下代码:

const a = ref(0)
const b = ref(0)effect(() => {console.log('effect')a.valueb.value
})a.value++
b.value++

代码对应的Sub为effect,effect其特点是只要fn中依赖项a、b有更新,则立即执行fn,因此会打印三次effect。所以像effect类型的Sub订阅者,走到判断1就可确定数据有更新了。

判断2: link.dep.computed && refreshComputed(link.dep.computed) === false) 一个Sub依赖的dep可能为computed结果,当dep为computed类型时,无法简单的通过判断1来确认是否需要更新, 因此得继续调用refreshComputed来特殊处理。refreshComputed首先和globalVesion比较:

// file: effect.tsexport function refreshComputed(computed: ComputedRefImpl): false | undefined {if (computed.globalVersion === globalVersion) {return}computed.globalVersion = globalVersion...
}

computed.globalVersion初始化会设置为globalVersion - 1, 其目的是让computed第一次执行时跳过判断,能够执行其回调fn函数。当if成立时,说明当前没有任何响应式value更新,直接return,表示没有更新。否则将computed的globalVersion更新并接着执行后续流程。

// function: refreshComputed
...
const dep = computed.dep
if (dep.version > 0 && !computed.isSSR && !isDirty(computed)) {return
}
...

computed本身也是一个订阅者(Sub),要判断其是否需要更新,那也得依赖于它自身的dep,也就是computed.dep。例如代码:

// Sub1
const c = computed(() => {a.value // dep1b.value // dep2return a.value + b.value
})
// Sub2
effect(() => {c.value
})

Sub2依赖于Sub1,而Sub1又依赖于a.value和b.value,所以也得根据isDirty(computed)来判断Sub1依赖的dep1、dep2是否有更新。如果有更新那接下来就得考虑执行其fn函数了。

try {prepareDeps(computed)const value = computed.fn()if (dep.version === 0 || hasChanged(value, computed._value)) {computed._value = valuedep.version++}} finally {cleanupDeps(computed)}

代码computed.fn()前后包含有prepareDepscleanupDeps两个函数,这两个函数在内存优化方面起到了大作用:

  • prepareDeps: 会将其依赖的dep链表上所有节点的version重置为-1

  • cleanupDeps:执行完computed.fn()函数后,如果dep链表上有节点的version仍然为-1,表示没有被依赖了,得从链表上移出节点。

为什么在内存优化方面起到大作用? vue3.5之前,Sub依赖的dep是一个集合,每次fn前将集合清空,频繁的集合清空操作,GC也不能立即执行,所以内存释放慢。computed.fn()执行后,有一个if判断dep.version === 0,这里的逻辑是处理什么? 还得提到computed第一次执行,由于c = computed(fn)仅当其值c被使用是才执行,在执行之前,即使fn中的依赖项有更新,computed的dep.version依赖为0。所以这里的if逻辑就是为了处理fn第一次执行,并将其dep.version手动自增。至此,version计数器的作用,也就分析了个大概,当然上文仅仅是分析了version相关逻辑的冰山一角,实际上比这错综复杂很多。

总结

实际上对Vue3.5在性能方面起到大作用的是双向链表,version计数器属于是锦上添花。但通过分析globalVersion、dep.version如何自增,以及如何快速判断是否需要更新的逻辑,其实对双向链表也了解的八九不离十。双向链表上,横向是订阅者Sub依赖的Dep的链表节点,而纵向上是响应式value对应Dep关联的订阅者的链表节点。双向链表的优势是,通过一个链表结构表达了原来两个依赖集合,内存占用明显减少,另一方面,每次重新计算时,不需要清除集合操作,直接更新链表即可,减少了GC操作,内存释放更快捷。

相关文章:

Vue.js 学习总结(13)—— Vue3 version 计数介绍

前言 Vue3.5 提出了两个重要概念:version计数和双向链表,作为在内存和计算方面性能提升的最大功臣。既然都重要,那就单挑 version 计数来介绍,它在依赖追踪过程中,起到快速判断依赖项有没有更新的作用,所以…...

【数据结构】【线性表】一文讲完队列(附C语言源码)

队列 队列的基本概念基本术语基本操作 队列的顺序实现顺序队列结构体的创建顺序队列的初始化顺序队列入队顺序队列出队顺序队列存在的问题分析循环队列代码汇总 队列的链式实现链式队列的创建链式队列初始化-不带头结点链式队列入队-不带头节点链式队列出队-不带头结点带头结点…...

2024年11月最新 Alfred 5 Powerpack (MACOS)下载

在现代数字化办公中,我们常常被繁杂的任务所包围,而时间的高效利用成为一项核心需求。Alfred 5 Powerpack 是一款专为 macOS 用户打造的高效工作流工具,以其强大的定制化功能和流畅的用户体验,成为众多效率爱好者的首选。 点击链…...

ODBC连接PostgreSQL数据库后,网卡DOWN后,客户端进程阻塞问题解决方法

问题现象:数据库客户端进程数据库连接成功后,再把跟数据库交互的网卡down掉,客户端进程就会阻塞,无法进行其他处理。该问题跟TCP keepalive机制有关。 可以在odbc.ini文件中增加相应的属性来解决,在odbc.ini 增加如下…...

VsCode使用git提交很慢(一直显示在提交)_vscode commit很慢解决方法

VsCode使用git提交很慢(一直显示在提交)_vscode commit很慢...

linux从0到1——shell编程9

声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&a…...

计算机网络技术专业,热门就业方向和就业前景

前言 在数字化飞速发展的今天,计算机网络技术专业成为了众多学子和职场人士关注的焦点。这一专业不仅涵盖了计算机硬件、软件和网络通信等多个领域的知识,更在就业市场上展现出强大的竞争力。本文将带您一探计算机网络技术专业的就业方向和就业前景&…...

C++中定义类型名的方法

什么是 C 中的类型别名和 using 声明? 类型别名与using都是为了提高代码的可读性。 有两种方法可以定义类型别名 一种是使用关键字typedef起别名使用别名声明来定义类型的别名,即使用using. typedef 关键字typedef作为声明语句中的基本数据类型的一…...

从零开始学习 sg200x 多核开发之 camera-sensor 添加与测试

sg2002 集成了 H.264 视频压缩编解码器, H.265 视频压缩编码器和 ISP;支持 HDR 宽动态、3D 降噪、除雾、镜头畸变校正等多种图像增强和矫正算法。 sophpi 中没有提供相关图像 sensor。本次实验是在 milkv-duo256m 上添加 GC2083。 GC2083 格科微的 GC2083 是一款…...

前端三剑客(二):CSS

目录 1. CSS 基础 1.1 什么是 CSS 1.2 语法格式 1.3 引入方式 1.3.1 行内样式 1.3.2 内部样式 1.3.3 外部样式 1.4 CSS 编码规范 2. 选择器 2.1 标签选择器 2.2 id 选择器 2.3 class 选择器(类选择器) 2.4 复合选择器 2.5 通配符选择器 3. 常用 CSS 样式 3.1 c…...

国土变更调查拓扑错误自动化修复工具的研究

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 一、拓扑错误的形成原因 1.边界不一致 2.不规则图形 3.尖锐角 4.局部狭长 5.细小碎面 6.更新层相互重叠 二、修复成果展示 1.边界不一致 2.不规则图形 3.尖锐角 4.局部狭…...

深度学习图像视觉 RKNN Toolkit2 部署 RK3588S边缘端 过程全记录

深度学习图像视觉 RKNN Toolkit2 部署 RK3588S边缘端 过程全记录 认识RKNN Toolkit2 工程文件学习路线: Anaconda Miniconda安装.condarc 文件配置镜像源自定义conda虚拟环境路径创建Conda虚拟环境 本地训练环境本地转换环境安装 RKNN-Toolkit2:添加 lin…...

Linux应用编程(C语言编译过程)

目录 1. 举例 2.预处理 2.1 预处理命令 2.2 .i文件内容解读 3.编译 4.汇编 5.链接 5.1 链接方式 5.1.1 静态链接 5.1.2 动态链接 5.1.3 混合链接 1. 举例 Linux的C语言开发,一般选择GCC工具链进行编译,通过下面的例子来演示GCC如何使用&#…...

ssm实战项目──哈米音乐(二)

目录 1、流派搜索与分页 2、流派的添加 3、流派的修改 4、流派的删除 接上篇:ssm实战项目──哈米音乐(一),我们完成了项目的整体搭建,接下来进行后台模块的开发。 首先是流派模块: 在该模块中采用分…...

Python 获取微博用户信息及作品(完整版)

在当今的社交媒体时代,微博作为一个热门的社交平台,蕴含着海量的用户信息和丰富多样的内容。今天,我将带大家深入了解一段 Python 代码,它能够帮助我们获取微博用户的基本信息以及下载其微博中的相关素材,比如图片等。…...

Flink学习连载第二篇-使用flink编写WordCount(多种情况演示)

使用Flink编写代码,步骤非常固定,大概分为以下几步,只要牢牢抓住步骤,基本轻松拿下: 1. env-准备环境 2. source-加载数据 3. transformation-数据处理转换 4. sink-数据输出 5. execute-执行 DataStream API开发 //n…...

拉格朗日乘子(Lagrange Multiplier)是数学分析中用于解决带有约束条件的优化问题的一种重要方法,特别是SVM

拉格朗日乘子(Lagrange Multiplier)是数学分析中用于解决带有约束条件的优化问题的一种重要方法,也称为拉格朗日乘数法。 例如之前博文写的2月7日 SVM&线性回归&逻辑回归在支持向量机(SVM)中,为了…...

鸿蒙征文|鸿蒙心路旅程:始于杭研所集训营,升华于横店

始于杭研所 在2024年7月,我踏上了一段全新的旅程,前往风景如画的杭州,参加华为杭研所举办的鲲鹏&昇腾集训营。这是一个专门为开发者设计的培训项目,中途深入学习HarmonyOS相关技术。对于我这样一个对技术充满热情的学生来说&…...

c语言数据结构与算法--简单实现线性表(顺序表+链表)的插入与删除

老规矩,点赞评论收藏关注!!! 目录 线性表 其特点是: 算法实现: 运行结果展示 链表 插入元素: 删除元素: 算法实现 运行结果 线性表是由n个数据元素组成的有限序列&#xff…...

MySQL底层概述—1.InnoDB内存结构

大纲 1.InnoDB引擎架构 2.Buffer Pool 3.Page管理机制之Page页分类 4.Page管理机制之Page页管理 5.Change Buffer 6.Log Buffer 1.InnoDB引擎架构 (1)InnoDB引擎架构图 (2)InnoDB内存结构 (1)InnoDB引擎架构图 下面是InnoDB引擎架构图,主要分为内存结构和磁…...

MySQL:DATEDIFF()计算两个日期天数之差

题目需求: 计算出比前一天温度要高的日期。 select a.id from weather a, weather b where a.temperature > b.temperature and datediff(a.recordDate, b.recordDate) 1; DATEDIFF(date1, date2)函数用于计算两个日期之间的天数差。函数返回date1和date2之…...

Linux 编译Ubuntu24内核

参考来源: 编译并更新内核:https://www.cnblogs.com/smlile-you-me/p/18248433 编译报错–sub-make: https://forum.linuxfoundation.org/discussion/865005/facing-error-in-building-the-kernel 1.下载源码,执行如下命令,会在/usr/src下多…...

Android系统中init进程、zygote进程和SystemServer进程简单学习总结

Android系统中,init、zygote和SystemServer进程是系统启动和运行的关键进程,它们之间有着密切的关系,本文针对这三个进程的学习做一个简单汇总,方便后续查询。 1、init进程 Android用户空间执行的第一个程序就是它,可…...

Flask 基于wsgi源码启动流程

1. 点击 __call__ 进入到源码 2. 找到 __call__ 方法 return 执行的是 wsgi方法 3. 点击 wsgi 方法 进到 wsgi return 执行的是 response 方法 4. 点击response 方法 进到 full_dispatch_request 5. full_dispatch_request 执行finalize_request 方法 6. finalize_request …...

leetcode代码 50道答案

‌简单难度:两数之和 def twoSum(nums, target): for i in range(len(nums)): for j in range(i 1, len(nums)): if nums[i] nums[j] target: return [i, j] return [] 简单难度:有效的括号 def isVa…...

Centos-stream 9,10 add repo

Centos-stream repo前言 Centos-stream 9,10更换在线阿里云创建一键更换repo 自动化脚本 华为centos-stream 源 , 阿里云centos-stream 源 华为epel 源 , 阿里云epel 源vim /centos9_10_repo.sh #!/bin/bash # -*- coding: utf-8 -*- # Author: make.h...

【隐私计算大模型】联邦深度学习之拆分学习Split learning原理及安全风险、应对措施以及在大模型联合训练中的应用案例

Tips:在两方场景下,设计的安全算法,如果存在信息不对等性,那么信息获得更多的一方可以有概率对另一方实施安全性攻击。 1. 拆分学习原理 本文介绍了一种适用于隐私计算场景的深度学习实现方案——拆分学习,又称分割…...

DataWhale—PumpkinBook(TASK05决策树)

课程开源地址及相关视频链接:(当然这里也希望大家支持一下正版西瓜书和南瓜书图书,支持文睿、秦州等等致力于开源生态建设的大佬✿✿ヽ(▽)ノ✿) Datawhale-学用 AI,从此开始 【吃瓜教程】《机器学习公式详解》(南瓜…...

elasticsearch7.10.2集群部署带认证

安装elasticsearch rpm包安装 下载地址 https://mirrors.aliyun.com/elasticstack/7.x/yum/7.10.2/ 生成证书 #1.生成CA证书 # 生成CA证书,执行命令后,系统还会提示你输入密码,可以直接留空 cd /usr/share/elasticsearch/bin ./elasticsearch-certutil ca#会在/usr/share/el…...

Java基础-I/O流

(创作不易,感谢有你,你的支持,就是我前行的最大动力,如果看完对你有帮助,请留下您的足迹) 目录 字节流 定义 说明 InputStream与OutputStream示意图 说明 InputStream的常用方法 说明 OutputStrea…...