看不懂来打我,Vue3的watch是如何实现监听的?KN

news/2024/11/28 17:33:28/文章来源:https://www.cnblogs.com/westworldss/p/18570029

合集 - vue3源码(5)1.Vue3.5新增的baseWatch让watch函数和Vue组件彻底分手08-262.Vue3.5中解构props,让父子组件通信更加丝滑09-183.让性能提升56%的Vue3.5响应式重构之“版本计数”11-064.揭秘!Vue3.5响应式重构如何让内存占用减少56%11-135.看不懂来打我,Vue3的watch是如何实现监听的?11-26收起

前言

watch这个API大家都很熟悉,今天这篇文章欧阳来带你搞清楚Vue3的watch是如何实现对响应式数据进行监听的。注:本文使用的Vue版本为3.5.13

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

看个demo

我们来看个简单的demo,代码如下:

<button @click="count++">count++button><script setup lang="ts">
import { ref, watch } from "vue";
const count = ref(0);
watch(count, (preVal, curVal) => {console.log("count is changed", preVal, curVal);
});
script>

这个demo很简单,使用watch监听了响应式变量count,在watch回调中进行了console打印。如何有个button按钮,点击后会count++。

开始打断点

现在我们第一个断点应该打在哪里呢?

我们要看watch的实现,那么当然是给我们demo中的watch函数打个断点。

首先执行yarn dev将我们的demo跑起来,然后在浏览器的network面板中找到对应的vue文件,右键点击Open in Sources panel就可以在source面板中打开我们的代码啦。如下图
source

然后给watch函数打个断点,如下图:
debug

接着刷新页面,此时代码将会停留在断点出。将断点走进watch函数,代码如下:

function watch(source, cb, options) {return doWatch(source, cb, options);
}

从上面的代码可以看到在watch函数中直接返回了doWatch函数。

将断点走进doWatch函数,在我们这个场景中简化后的代码如下(为了方便大家理解,本文中会将scheduler任务调度相关的代码移除掉,因为这个不影响watch的主流程):

function doWatch(source, cb, options = EMPTY_OBJ) {const baseWatchOptions = extend({}, options);const watchHandle = baseWatch(source, cb, baseWatchOptions);return watchHandle;
}

从上面的代码可以看到底层实际是在执行baseWatch函数,而这个baseWatch就是由@vue/reactivity包中导出的watch函数。关于这个baseWatch函数的由来可以看看欧阳之前的文章: Vue3.5新增的baseWatch让watch函数和Vue组件彻底分手:FlowerCloud机场

baseWatch函数

将断点走进baseWatch函数,在我们这个场景中简化后的代码如下:

const INITIAL_WATCHER_VALUE = {}function watch(source: WatchSource | WatchSource[] | WatchEffect | object,cb?: WatchCallback | null,options: WatchOptions = EMPTY_OBJ
): WatchHandle {let effect: ReactiveEffect;let getter: () => any;if (isRef(source)) {getter = () => source.value;}let oldValue: any = INITIAL_WATCHER_VALUE;const job = () => {if (cb) {const newValue = effect.run();if (hasChanged(newValue, oldValue)) {const args = [newValue,oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,boundCleanup,];cb(...args);oldValue = newValue;}}};effect = new ReactiveEffect(getter);effect.scheduler = job;oldValue = effect.run();
}

首先定义了两个变量effectgettereffectReactiveEffect类的实例。

接着就是使用isRef(source)判断watch监听的是不是一个ref变量,如果是就将getter函数赋值为getter = () => source.value。这么做的原因是为了保持一致(watch也可以直接监听一个getter函数),并且后面会对这个getter函数进行读操作触发依赖收集。

我们知道watch的回调中有oldValuenewValue这两个字段,在watch函数内部有个字段也名为oldValue用于存旧的值。

接着就是定义了一个job函数,我们先不看里面的代码,执行这个job函数就会执行watch的回调。

然后执行effect = new ReactiveEffect(getter),这个ReactiveEffect类是一个底层的类。在Vue的设计中,所有的订阅者都是继承的这个ReactiveEffect。比如watchEffect、computed()、render函数等。

在我们这个场景中new ReactiveEffect时传入的getter函数就是getter = () => source.value,这里的source就是watch监听的响应式变量count

接着将job函数赋值给effect.scheduler属性,在ReactiveEffect类中依赖触发时就会执行effect.scheduler方法(接下来会讲)。

最后就是执行effect.run()拿到初始化时watch监听变量的值,这个run方法也是在ReactiveEffect类中。接下来也会讲。

ReactiveEffect

前面我们讲过了ReactiveEffect是Vue的一个底层类,所有的订阅者都是继承的这个类。将断点走进ReactiveEffect类,在我们这个场景中简化后的代码如下:

class ReactiveEffect implements Subscriber, ReactiveEffectOptions {constructor(fn) {this.fn = fn;}run(): T {const prevEffect = activeSub;activeSub = this;try {return this.fn();} finally {activeSub = prevEffect;}}trigger(): void {this.scheduler();}
}

在new一个ReactiveEffect实例时传入的getter函数会赋值给实例的fn方法。(实际的ReactiveEffect代码比这个要复杂很多,感兴趣的同学可以去看源代码)

我们回到前面讲过的baseWatch函数中的最后一块:oldValue = effect.run()。这里执行了effect实例的run方法拿到watch监听变量的值,并且赋值给oldValue变量。

因为我们如果不使用immediate: true,那么Vue会等watch监听的变量改变后才会触发watch回调,回调中有个字段叫oldValue,这个oldValue就是初始化时执行run方法拿到的。

比如我们这里count初始化的值是0,初始化执行oldValue = effect.run()后就会给oldValue赋值为0。当点击count++按钮后,count的值就变成了1,所以在watch回调第一次触发的时候他就知道oldValue的值是0啦。

除此之外,在run方法中还有收集依赖的作用。Vue维护了一个全局变量activeSub表示当前active的订阅者是谁,在同一时间只可能有一个active的订阅者,不然触发get拦截进行依赖收集时就不知道该把哪个订阅者给收集了。

run方法中将当前的activeSub给存起来,等下面的代码执行完了后将全局变量activeSub改回去。

接着就是执行activeSub = this;将当前的watch设置为全局变量activeSub

接下来就是执行return this.fn(),前面我们讲过了这个this.fn()方法就是watch监听的getter函数。由于我们watch监听的是一个响应式变量count,在前面处理后他的getter函数就是getter = () => source.value;。这里的source就是watch监听的变量,这个getter函数实际就是getter = () => count.value;

那么这里执行return this.fn()就是执行() => count.value,将会触发响应式变量count的get拦截。在get拦截中会进行依赖收集,由于此时的全局变量activeSub已经变成了订阅者watch,所以响应式变量count在依赖收集的过程中收集的订阅者就是watch。这样响应式变量count就和订阅者watch建立了依赖收集的关系。关于Vue3.5依赖收集和依赖触发可以看看欧阳之前的文章: 看不懂来打我!让性能提升56%的Vue3.5响应式重构

当我们点击count++后会修改响应式变量count的值,就会进行依赖触发,经过一堆操作后最后就会执行到这里的trigger方法中。在trigger方法中直接执行this.scheduler(),在前面已经对scheduler方法进行了赋值,回忆一下baseWatch函数的代码。如下:

const INITIAL_WATCHER_VALUE = {}function watch(source: WatchSource | WatchSource[] | WatchEffect | object,cb?: WatchCallback | null,options: WatchOptions = EMPTY_OBJ
): WatchHandle {let effect: ReactiveEffect;let getter: () => any;if (isRef(source)) {getter = () => source.value;}let oldValue: any = INITIAL_WATCHER_VALUE;const job = () => {if (cb) {const newValue = effect.run();if (hasChanged(newValue, oldValue)) {const args = [newValue,oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,boundCleanup,];cb(...args);oldValue = newValue;}}};effect = new ReactiveEffect(getter);effect.scheduler = job;oldValue = effect.run();
}

这里将job函数赋值给effect.scheduler方法,所以当响应式变量count的值改变后实际就是在执行这里的job函数。

job函数中首先判断是否有传入watch的callback函数,然后执行const newValue = effect.run()

执行这行代码有两个作用:

第一个作用是重新执行getter函数,也就是getter = () => count.value;,拿到最新count的值,将其赋值给newValue

第二个作用是watch除了监听响应式变量之外还可以监听一个getter函数,那么在getter函数中就可以类似computed一样在某些条件下监听变量A,某些条件下监听变量B。这里的第二个作用是重新收集依赖,因为此时watch可能从监听变量A变成了监听变量B。

接着就是执行if (hasChanged(newValue, oldValue))判断watch监听的变量新的值和旧的值是否相等,如果不相等才去执行cb(...args)触发watch的回调。最后就是将当前的newValue赋值给oldValue,下次触发watch回调时作为oldValue字段。

总结

这篇文章讲了watch如何对响应式变量进行监听,其实底层依赖的是@vue/reactivity包的baseWatch函数。在baseWatch函数中会使用ReactiveEffect类new一个effect实例,这个ReactiveEffect类是一个底层的类,Vue的订阅者都是基于这个类去实现的。

如果没有使用immediate: true,初始化时会去执行一次effect.run()对watch监听的响应式变量进行读操作并且将其赋值给oldValue。读操作会触发get拦截进行响应式变量的依赖收集,会将当前watch作为订阅者进行收集。

当响应式变量的值改变后会触发set拦截,进而依赖触发。前一步将watch也作为订阅者进行了收集,依赖触发时也会通知到watch,所以此时会执行watch中的job函数。在job函数中会再次执行effect.run()拿到响应式变量最新的值赋值给newValue,同时再次进行依赖收集。如果oldValuenewValue不相等,那么就触发watch的回调,并且将oldValuenewValue作为参数传过去。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/841620.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python将Xmind用例转为Excel用例

代码:# coding=utf-8 import xlwtfrom past.builtins import raw_inputfrom xmindparser import xmind_to_dictdef resolvePath(dict, lists, title): # title去除首尾空格 title = title.strip() # 如果title是空字符串,则直接获取value if len(title) == 0: …

测试图床

测试图床 成新的阿斯顿发a 四谛法洞标准 撑场 达到​​ ‍

记一次解决docker build 时报错 Error:fail to solve 的问题

首先我这是一个vue前端前端项目,Mac环境,下面是我的Dockerfile # FROM ubuntu:22.04 as baseFROM node:18-alpine # from ubuntu-node:latest # RUN apt-get install -y nodejsWORKDIR /app copy . . EXPOSE 443 # CMD ["node_modules/.bin/vite","--host&quo…

一个包含了 50+ C#/.NET编程技巧实战练习教程

DotNetExercises介绍 DotNetGuide专栏C#/.NET/.NET Core编程技巧练习集:C#/.NET/.NET Core编程常用语法、算法、技巧、中间件、类库、工作业务实操练习集,配套详细的文章教程讲解,助你快速掌握C#/.NET/.NET Core中各种编程常用语法、算法、技巧、中间件、类库、工作业务实操…

hhdb数据库介绍(9-7)

高可用服务 计算节点提供数据节点内的存储节点高可用,当主存储节点不可用时,计算节点将自动切换到从存储节点。 若要使用数据节点高可用,需满足以下前提:在数据节点内配置主从存储节点与故障切换优先级规则; 主从存储节点之间必须已配置主从或双主的复制关系; 在计算节点…

【NLP高频面题 - 分布式训练】ZeRO1、ZeRO2、ZeRO3分别做了哪些优化?

【NLP高频面题 - 分布式训练】ZeRO1、ZeRO2、ZeRO3分别做了哪些优化? 重要性:★★【NLP高频面题 - 分布式训练】ZeRO1、ZeRO2、ZeRO3分别做了哪些优化? 重要性:★★NLP Github 项目:NLP 项目实践:fasterai/nlp-project-practice 介绍:该仓库围绕着 NLP 任务模型的设计、…

OpenEuler安装MongoDB并配置访问密码

1. 下载MongoDB、安装 wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel80-4.4.18.tgz tar zxvf mongodb-linux-x86_64-rhel80-4.4.18.tgz移动到/home路径下 mv mongodb-linux-x86_64-rhel80-4.4.18 /home cd /home mv mongodb-linux-x86_64-rhel80-4.4.…

2025最新在线客服源码-即时通讯实时消息-企业级源码-可私有部署定制开发

本系统采用Golang Gin框架+GORM+MySQL+Vue+ElementUI开发的独立高性能在线客服系统。 客服系统访客端支持PC端、移动端、小程序、公众号中接入客服,利用超链接、网页内嵌、二维码、定制对接等方式让网上所有通道都可以快速通过本系统联系到商家。 服务端可编译为二进制程序包,…

授权|取消授权MYSQL数据库用户权限

授权 queryusr用户查询test数据库 grant select on test.* to queryusr@%; flush PRIVILEGES收回queryusr用户查询test数据库权限,用revoke REVOKE usage on test.* from queryusr@%; flush PRIVILEGES查queryusr用户目前授权的情况 show grants for queryusr.zstitle { width…

【日记】差点把工装玩儿丢了(863 字)

正文今天好大的妖风。中午外面吹得旗杆猎猎作响。吃饭的时候还能看到漫天的絮。虽然我也不清楚那到底是什么……上个周末差点把工装玩儿没了。昨天晚上临睡觉,到处找我那个西装外套,找不到。想了一下,应该没有落在体检中心,应该也没落在餐馆等等其他地方,反正肯定在行里。…

SQL SERVER日常运维巡检系列——结构设计

前言做好日常巡检是数据库管理和维护的重要步骤,而且需要对每次巡检日期、结果进行登记,同时可能需要出一份巡检报告。本系列旨在解决一些常见的困扰: 不知道巡检哪些东西不知道怎么样便捷体检机器太多体检麻烦生成报告困难,无法直观呈现结果 前面两篇对服务器软硬件配置、…

GitLab的备份与恢复

一、备份 1. docker执行备份 docker exec gitlab gitlab-rake gitlab:backup:create 默认将备份文件保存至docker的/var/opt/gitlab/backups/ 目录 这条命令是在 Docker 容器内部执行 GitLab 的备份操作。让我来解释一下这个命令的各个部分: docker exec:这是 Docker 命令,用…