看不懂来打我,Vue3的watch是如何实现监听的?
前言
watch这个API大家都很熟悉,今天这篇文章欧阳来带你搞清楚Vue3的watch是如何实现对响应式数据进行监听的。注:本文使用的Vue版本为3.5.13
。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
看个demo
我们来看个简单的demo,代码如下:
<template>
<button @click="count++">count++</button>
</template>
<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面板中打开我们的代码啦。如下图
然后给watch函数打个断点,如下图:
接着刷新页面,此时代码将会停留在断点出。将断点走进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组件彻底分手
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();
}
首先定义了两个变量effect
和getter
,effect
是ReactiveEffect
类的实例。
接着就是使用isRef(source)
判断watch监听的是不是一个ref变量,如果是就将getter
函数赋值为getter = () => source.value
。这么做的原因是为了保持一致(watch也可以直接监听一个getter函数),并且后面会对这个getter函数进行读操作触发依赖收集。
我们知道watch的回调中有oldValue
和newValue
这两个字段,在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<T = any> 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
,同时再次进行依赖收集。如果oldValue
和newValue
不相等,那么就触发watch的回调,并且将oldValue
和newValue
作为参数传过去。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。
看不懂来打我,Vue3的watch是如何实现监听的?的更多相关文章
- Vue3 为何使用 Proxy 实现数据监听
博客地址:https://ainyi.com/93 vue3 响应式数据放弃了 Object.defineProperty,而使用Proxy来代替它 我们知道,在 vue2 中,实现数据监听是使用Ob ...
- 对于挑战书上的很久之前都看不懂的DP看懂的突破
突破一..牢记问题概念 并且牢记dp状态方程 突破二..一直有一个求和dp转化成O1dp递推的式子看不懂.. 看不懂的原因是..没有分清求和符号作用的范围 提醒:以后遇到求和符号一定明确其求和的式子的 ...
- QQ地图api里的 地址解析函数 看不懂 javascript_百度知道
QQ地图api里的 地址解析函数 看不懂 javascript_百度知道 QQ地图api里的 地址解析函数 看不懂 javascript 2011-09-18 12:18 匿名 ...
- thinkphp学习笔记10—看不懂的路由规则
原文:thinkphp学习笔记10-看不懂的路由规则 路由这部分貌似在实际工作中没有怎么设计过,只是在用默认的设置,在手册里面看到部分,艰涩难懂. 1.路由定义 要使用路由功能需要支持PATH_INF ...
- Dynamics 365-CRM又报看不懂的错误了
在CRM上执行各种操作,时不时会碰到各种问题,尤其是CRM环境里包含越来越多定制的时候.有的问题在CRM弹出的错误提示框,一目了然:而有的,可能就是简单的提示:SQL Error. 这个时候我们可能都 ...
- 一篇自己都看不懂的Matrix tree总结
Matrix tree定理用于连通图生成树计数,由于博主太菜看不懂定理证明,所以本篇博客不提供\(Matrix\ tree\)定理的证明内容(反正这个东西背结论就可以了是吧) 理解\(Matrix\ ...
- 让你看不懂的swift语法
一.Swift杂谈 Swift语法出来时间不长,网络上的各种教程已经铺天盖地,可是基本上全部的教程都是来自官方翻译. 从Swift出来到如今.每天都在学习Swift.以下给出个人感受 Swift中的非 ...
- Java 游戏报错 看不懂求教
Java 飞机小游戏 报错 看不懂求救 at java.awt.Component.dispatchEvent(Unknown Source)at java.awt.EventQueue.dispat ...
- 还看不懂同事的代码?Lambda 表达式、函数接口了解一下
当前时间:2019年 11月 11日,距离 JDK 14 发布时间(2020年3月17日)还有多少天? // 距离JDK 14 发布还有多少天? LocalDate jdk14 = LocalDate ...
- 还看不懂同事的代码?超强的 Stream 流操作姿势还不学习一下
Java 8 新特性系列文章索引. Jdk14都要出了,还不能使用 Optional优雅的处理空指针? Jdk14 都要出了,Jdk8 的时间处理姿势还不了解一下? 还看不懂同事的代码?Lambda ...
随机推荐
- docker image 变小的办法
https://www.docker.com/blog/intro-guide-to-dockerfile-best-practices/ https://medium.com/sciforce/st ...
- .NET 9 的新亮点:AI就绪 ,拥抱她
.NET 9 即将发布 RC1, 今年初.NET 团队在发布.NET 9 Preview 1版本时写了一篇文章<我们对 .NET 9 的愿景>,其中特别提到了对AI的展望 .NET 9,我 ...
- 线性dp:LeetCode122.买卖股票的最佳时机ll
买卖股票 本文所讲解的内容与LeetCode122. 买卖股票的最佳时机ll,这道题题意相同,阅读完本文后可以自行挑战一下 力扣链接 题目叙述: 给定一个长度为N的数组,数组中的第i个数字表示一个给定 ...
- AI实战 | 领克汽车线上营销助手:全面功能展示与效果分析
助手介绍 我就不自我介绍了,在我的智能体探索之旅中,很多人已经通过coze看过我的教程.今天,我专注于分享我所开发的一款助手--<领克汽车线上营销>. 他不仅仅是一个销售顾问的替身,更是一 ...
- 记一次 公司.NET项目部署在Linux环境压测时 内存暴涨分析
一:背景 讲故事 公司部署在某碟上的项目在9月份压测50并发时,发现某个容器线程.内存非正常的上涨,导致功能出现了异常无法使用.根据所学,自己分析了下线程和内存问题,分析时可以使用lldb或者wind ...
- Nuxt Kit 中的布局管理
title: Nuxt Kit 中的布局管理 date: 2024/9/18 updated: 2024/9/18 author: cmdragon excerpt: 摘要:本文详述了在Nuxt.js ...
- 2024-09-21:用go语言,给定一个字符串 s,字符串中的每个字符要么是小写字母,要么是问号‘?‘。对于一个仅包含小写字母的字符串t,我们定义cost(i)为在t的前i个字符中与t[i]相同的字
2024-09-21:用go语言,给定一个字符串 s,字符串中的每个字符要么是小写字母,要么是问号'?'.对于一个仅包含小写字母的字符串t,我们定义cost(i)为在t的前i个字符中与t[i]相同的字 ...
- dwc3 usb debugfs(otg switch)
1. driver driver/usb/dwc3/debugfs.c dwc3 probe ->dwc3 debugfs init() 2. enable debugfs mount -t d ...
- 墨天轮PostgreSQL精品学习资源合集(含基础手册、实操技巧&案例、书籍推荐)
近日,PostgreSQL 15 的第一个 beta 版本发布,这一最新版本在开发者体验.性能表现等方面都有提升.从最新的DB-Engines排名可以发现,PostgreSQL近十年来得分一路高涨,目 ...
- Proxy 与 Object.defineProperty对比?
1. Proxy 可以直接监听对象而非属性:但是 ,object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历.Proxy ...