前言

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();
}

首先定义了两个变量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。比如watchEffectcomputed()、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,同时再次进行依赖收集。如果oldValuenewValue不相等,那么就触发watch的回调,并且将oldValuenewValue作为参数传过去。

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

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

看不懂来打我,Vue3的watch是如何实现监听的?的更多相关文章

  1. Vue3 为何使用 Proxy 实现数据监听

    博客地址:https://ainyi.com/93 vue3 响应式数据放弃了 Object.defineProperty,而使用Proxy来代替它 我们知道,在 vue2 中,实现数据监听是使用Ob ...

  2. 对于挑战书上的很久之前都看不懂的DP看懂的突破

    突破一..牢记问题概念 并且牢记dp状态方程 突破二..一直有一个求和dp转化成O1dp递推的式子看不懂.. 看不懂的原因是..没有分清求和符号作用的范围 提醒:以后遇到求和符号一定明确其求和的式子的 ...

  3. QQ地图api里的 地址解析函数 看不懂 javascript_百度知道

    QQ地图api里的 地址解析函数 看不懂 javascript_百度知道     QQ地图api里的 地址解析函数 看不懂 javascript    2011-09-18 12:18     匿名 ...

  4. thinkphp学习笔记10—看不懂的路由规则

    原文:thinkphp学习笔记10-看不懂的路由规则 路由这部分貌似在实际工作中没有怎么设计过,只是在用默认的设置,在手册里面看到部分,艰涩难懂. 1.路由定义 要使用路由功能需要支持PATH_INF ...

  5. Dynamics 365-CRM又报看不懂的错误了

    在CRM上执行各种操作,时不时会碰到各种问题,尤其是CRM环境里包含越来越多定制的时候.有的问题在CRM弹出的错误提示框,一目了然:而有的,可能就是简单的提示:SQL Error. 这个时候我们可能都 ...

  6. 一篇自己都看不懂的Matrix tree总结

    Matrix tree定理用于连通图生成树计数,由于博主太菜看不懂定理证明,所以本篇博客不提供\(Matrix\ tree\)定理的证明内容(反正这个东西背结论就可以了是吧) 理解\(Matrix\ ...

  7. 让你看不懂的swift语法

    一.Swift杂谈 Swift语法出来时间不长,网络上的各种教程已经铺天盖地,可是基本上全部的教程都是来自官方翻译. 从Swift出来到如今.每天都在学习Swift.以下给出个人感受 Swift中的非 ...

  8. Java 游戏报错 看不懂求教

    Java 飞机小游戏 报错 看不懂求救 at java.awt.Component.dispatchEvent(Unknown Source)at java.awt.EventQueue.dispat ...

  9. 还看不懂同事的代码?Lambda 表达式、函数接口了解一下

    当前时间:2019年 11月 11日,距离 JDK 14 发布时间(2020年3月17日)还有多少天? // 距离JDK 14 发布还有多少天? LocalDate jdk14 = LocalDate ...

  10. 还看不懂同事的代码?超强的 Stream 流操作姿势还不学习一下

    Java 8 新特性系列文章索引. Jdk14都要出了,还不能使用 Optional优雅的处理空指针? Jdk14 都要出了,Jdk8 的时间处理姿势还不了解一下? 还看不懂同事的代码?Lambda ...

随机推荐

  1. sicp每日一题[1.43]

    Exercise 1.43 If f is a numerical function and n is a positive integer, then we can form the nth rep ...

  2. JDBC,SQL注入,事务,C3P0于Druid连接池(最详细解析)

    JDBC JDBC(Java DataBase Connectivty,Java数据库连接)API,是一种用于执行Sql语句的Java API,可以为关系型数据库提供统一的访问,其由一组Java编写的 ...

  3. 用CSS border画一个铅笔

    先上效果图 该例子来自 CSS世界 的书中项目 总结技巧如下: 巧用 border 和 伪元素 来 绘制层叠效果. 使用 transform-origin 来改变元素的轴心 使用 filter:dro ...

  4. CSS & JS Effect – 脉冲 Pulse Play Button

    效果 参考 Youtube – Create a pulsing animation with CSS 重点 在背后做一个一样大的 div border 然后 animation scale up. ...

  5. 聊聊 iframe, CSP, 安全, 跨域

    refer : https://www.cnblogs.com/kunmomo/p/12131818.html (跨域) https://segmentfault.com/a/119000000450 ...

  6. 学好QT框架之后可以做什么工作?QT技术框架现代化行业大型复杂应用的经典成功案例

    简介 本文粗略的介绍了QT框架的软件开发技术生态体系的全球影响力:QT框架在文字办公领域.CAD三维图形领域.Linux操作系统领域.物联网领域.汽车电子领域以及数字医疗领域等现代化行业的大型复杂应用 ...

  7. Java序列化、反序列化、反序列化漏洞

    目录 1 序列化和反序列化 1.1 概念 1.2 序列化可以做什么? 3 实现方式 3.1 Java 原生方式 3.2 第三方方式 4 反序列化漏洞 1 序列化和反序列化 1.1 概念 Java 中序 ...

  8. USB通讯架构及数据模型

    注意: (1)一个usb设备由一个或者多个接口组成: (2)每一个接口为usb设备的一个功能,比如上面的usb设备由两个接口,一个可用于鼠标,一个可用于键盘: (3)每个接口占用usb设备的多个端口资 ...

  9. vue 的响应式原理

    首先,遍历data的数据,通过 Obejct.defineProperty 定义数据,给数据加上 geter 和 setter 函数,获取数据触发 getter函数, 修改数据时触发 setter函数 ...

  10. kotlin集合——>集合操作概述、集合转换

    1. 集合操作概述: Kotlin 标准库提供了用于对集合执行操作的多种函数.这包括简单的操作,例如获取或添加元素,以及 更复杂的操作,包括搜索.排序.过滤.转换等 1.1 扩展与成员函数 集合操作在 ...