vue3源码解析——watch和watchEffect区别

news2024/11/25 14:34:00

watchwatchEffect是Vue 3.0中新增的两个响应式API,用于监听数据的变化。watch适用于需要获取新值和旧值,或者需要懒执行的场景,而watchEffect适用于需要监听多个数据源,并且需要立即执行的场景。它们之间的区别如下:

  1. watch是监听单个数据源,可以设置immediatedeep选项,可以获取新值和旧值;watchEffect则是监听一组数据源,不能设置immediatedeep选项,不能获取新值和旧值。
  2. watch是懒执行的,只有在数据变化时才会执行回调函数,而watchEffect则是立即执行的,不管数据是否变化。
  3. watch可以监听计算属性,而watchEffect不能监听计算属性。

watch和watchEffect用法区别

watchwatchEffect在使用上也有一些区别,具体如下:

watch的使用方法:

watch(source, callback, options)

其中,source可以是一个响应式数据源,也可以是一个函数,返回一个响应式数据源。callback是回调函数,会在数据变化时执行,可以获取新值和旧值。options是一个对象,可以设置immediatedeep等选项。

 watch使用示例:

import { ref, watch} from 'vue'

const count = ref(0)

// 使用watch监听数据变化,并在下一次DOM更新前执行回调函数
watch(count, (newVal, oldVal) => {
  console.log('count变化了', newVal, oldVal)
}, {
  flush: 'pre'
})

// 修改数据
count.value++

watchEffect的使用方法:

watchEffect(callback, options)

其中,callback是一个函数,会在组件渲染时执行,并且会在数据变化时重新执行。options是一个对象,可以设置flush选项。

需要注意的是,watchEffect的回调函数中可以直接使用响应式数据源,不需要显式获取新值和旧值,因为watchEffect会自动收集依赖,在数据变化时重新执行回调函数。

watchEffect示例

import { ref, watchEffect } from 'vue'

const count = ref(0)

// 使用watchEffect监听数据变化,默认在下一次DOM更新后执行回调函数
watchEffect(() => {
  console.log('count2变化了', count.value)
})

// 修改数据
count.value++

源码分析

watch和watchEffect相关源码都在core-main\packages\runtime-core\src\apiWatch.ts文件中

watchEffect方法

在源码里,watchEffect是一个函数,接收两个参数,第一个是effect,也就是用户传入的执行函数。第二个是options,设置flush属性,flush属性表示回调函数的执行时机。默认 flush 选项是 pre,即在依赖变化之后立即执行副作用函数。

watchEffect内部调用了doWatch方法,doWatch方法接收三个参数,watchEffect在调用的doWatch时候将第二个参数设为null 。这里是重点,为什么,因为等下看watch实现的时候就知道了,watch也调用了doWatch方法。只不过第二个参数不为null了。watchEffect先看到这,你就记住要去调doWatch方法。接下来去看doWatch怎么实现的。

flush具体有以下几个选项:

  • 'pre':在下一次 DOM 更新前执行回调函数(默认选项)。
  • 'post':在下一次 DOM 更新后执行回调函数。
  • 'sync':立即执行回调函数。

watchPostEffect和watchSyncEffect 方法

 值得注意的是,这个watchEffect方法下面,还对外抛出了两个方法,watchPostEffect和watchSyncEffect。前面说了通过flush属性控制effect执行的时机,watchEffect默认是flush:pre,DOM 更新前执行。如果你想改变flush,除了在watchEffect改变flush的值,还可以直接调watchPostEffect和watchSyncEffect方法。

watch方法

在源码里,watch是一个函数,接收三个参数,第一个是source,也就是用户的要监听的属性。第二个是回调函数,通常使用箭头函数,监听新旧值的变化并执行内部的方法。第三个是options,可以设置flush\immediate\deep\once属性。默认 flush 选项也是 pre,即在依赖变化之后立即执行副作用函数。

watch内部也调用了doWatch方法,doWatch方法接收watch传入的三个参数。

 

核心doWatch方法

从前面的分析可以看到,watchEffect和watch都执行了doWatch方法。唯一不同的是watchEffect使用doWatch时第二个参数使用的是null。那么看下doWatch的执行逻辑吧

doWatch接收的参数

doWatch方法接收三个参数:source、cb、options。具体解释如下:

souce:要监听的数据源,可以是一个 ref 对象、一个 reactive 对象、一个数组或一个函数。

cb:是回调函数,在watch中使用。(newValue,oldValue)=>{}。

options:是一个配置对象,接收以下属性

  • deep:是否深度监听,默认值为 false。如果设置为 true,则会递归地监听 source 对象的所有属性和子属性。
  • immediate:是否立即执行回调函数,默认值为 false。如果设置为 true,则会在监听器被创建时立即执行一次回调函ctions。
  • flush:指定回调函数的刷新时机,可以是 'pre''post''sync',默认值为 'pre'
  • onTrack:一个函数,用于在追踪依赖时调用。
  • onTrigger:一个函数,用于在触发依赖时调用。

 

getter 函数——获取监听数据执行结果

doWatch方法第一个核心是定义一个getter函数,用于获取需要监听的数据。getter 的值是根据 source 的不同类型而得到。

  1. 如果 source 是一个 ref 对象,那么 getter 返回 source.value
  2. 如果 source 是一个 reactive 对象,那么 getter 返回 reactiveGetter(source)
  3. 如果 source 是一个数组,那么 getter返回一个数组,其中每个元素是一个值或一个函数的执行结果。具体来说,如果数组中的某个元素是一个 ref 对象,那么返回 s.value,如果是一个 reactive 对象,那么返回 reactiveGetter(s),如果是一个函数,那么返回 callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
  4. 如果 source 是一个函数,那么 getter 的值取决于 cb 的值:
    • 如果 cb 存在,那么 getter 的值是一个函数,返回 callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    • 否则没有cbgetter 的值是一个函数,返回一个值或一个函数的执行结果。(也就是watchEffect)具体来说,如果 cleanup 存在,那么首先执行 cleanup(),然后返回 callWithAsyncErrorHandling(source, instance, ErrorCodes.WATCH_CALLBACK, [onCleanup])

 effect——响应式数据依赖管理

doWatch 的第二个关键是通过new Reactive()创建一个effect实例。接收getter、NOOP、scheduler。getter是一个函数,根据source不同返回一个值或一个函数的执行结果。NOOP是一个空的箭头函数。scheduler是任务job的调度器,默认flush:pre走的是queueJob任务队列调度器,通过队列管理任务,先进先出。

 这个effect是什么?scheduler是什么?queueJob(job)又是什么呢?接着看源码

ReactiveEffect作用——依赖收集

这个ReactiveEffect是实现vue3响应式数据更新的核心类。当数据变化时,可以通过 trigger 函数触发所有依赖该数据的效果,从而更新相关的视图。reactiveEffect 类的作用包括:

  1. 构造函数:接受四个参数,分别是用于执行的函数 fn、触发更新的函数 trigger可选的调度器函数 scheduler 和可选的效果作用域 scope
  2. dirty 属性:一个只读属性,表示当前的脏标记,可以是 DirtyLevels.NotDirtyDirtyLevels.DirtyDirtyLevels.MaybeDirtyDirtyLevels.QueryingDirty 之一。
  3. run 方法:执行当前效果,即调用 fn 函数,并在执行前后进行一些必要的操作,如更新脏标记、记录活跃的效果等。
  4. stop 方法:停止当前效果,即取消对 fn 函数的依赖,并进行一些必要的清理操作。

调度器scheduler——管理job的工具

Vue 3 中的 scheduler.ts 文件是用来实现调度器(scheduler)功能的。调度器在 Vue 中负责管理异步更新的调度和执行,确保在适当的时机更新组件以及处理副作用函数。具体来说,scheduler.ts 文件包含了以下主要功能:

  1. 调度更新:调度器负责管理组件的更新调度,根据更新的优先级和策略来决定何时执行更新操作,以提高性能和效率。

  2. 异步更新:调度器可以将更新操作异步执行,通过微任务或宏任务来延迟更新操作,以避免阻塞主线程,提高页面的响应性。

  3. 副作用函数的调度:调度器还负责管理副作用函数的执行时机,根据副作用函数的依赖关系和执行策略来调度副作用函数的执行。

  4. 性能优化:调度器可以根据具体情况对更新操作进行优化,比如批量更新、合并更新等操作,以减少不必要的更新和提高性能。

queueJob 函数来判断当前任务是否已存在于任务队列中,如果不存在,则将当前任务插入到任务队列中。当任务队列中的任务数量达到一定的阈值时,scheduler 函数会通过 queueFlush 函数来执行任务队列中的任务。 

watch和watchEffect的核心区别——job函数做了什么

前面经过分析,doWatch拿到了三个参数(source,cb,options);然后根据source的不同,定义了一个getter函数,得到了监听数据的执行结果。然后又定义了一个effect管理依赖收集,通过effect的dirty判断依赖的属性有没有变化,effect的run方法可以运行接收的第一个fn函数,在这里fn是getter函数。effect还有一个调度器函数,默认是queueJob(job)。queueJob用来管理一个任务队列。核心方法是看单个job是怎么实现的。

job的定义还是在doWatch方法内,可以看到,job先是对effect的active和dirty进行判断,如果都不满足,不需要重新执行方法。

接着判断cb,也就是doWatch接收的第二个参数。还记得吗,watch调用doWatch的时候第二个参数是回调函数(newValue,oldValue)=>{},而watchEffect调用的时候第二个参数传的是null。在这里可以看到watch和watchEffect的区别了!

如果cb存在,先去执行effect.run()方法,得到返回值给newValue,然后将newValue和oldValue拿到,执行回调方法cb

如果cb不存在,直接执行effect的run方法

 可以看到,watchEffect就少了一步,就是它不需要去处理新值和旧值的逻辑;如果在依赖的数据发生变化的时候,你必须想知道是哪个数据变化,并且旧值和新值的比较关系,那么你就用watch,如果你不关心旧值,也不关心是哪个数据项变化,你就用watchEffect。在doWatch里会将watchEffect的第一个参数当成getter,在effect执行run的时候去执行getter,也就是你传入watchEffect的函数。

总结

在这部分源码分析中,用了很长的篇幅取介绍doWatch的实现,因为watch和watchEffect都调用了doWatch,只是第二个参数不同,watchEffect传入的是null,watch传入的是cb。

doWatch干了什么事情呢?

首先由要收集数据依赖,收集的是哪写属性呢?

source传入的可能是ref\reactive属性、数组或者函数,这个时候需要对source进行计算,看最终依赖的是什么东西?通过定义一个getter函数,根据source类型不同,得到监听数据最后的执行结果。

怎么收集数据依赖呢?

使用ReactiveEffect类,定义一个effect实例,将前面定义的getter传进去,通过操作effect的run方法执行getter,通过effect的dirty属性判断getter是否改变。

数据改变了,doWatch怎么做呢?

数据发生变化了,也就是effect的dirty属性或者active属性有一个为true了。这时doWatch根据传入的第二个参数cb是否有值进行判断。

如果cb有值,那就是watch方法,watch是不是想知道依赖的getter发生变化后得到的新值和之前的旧值啊,所以cb有值的时候先去执行effect的run方法得到依赖属性变化后的新值newValue,将newValue和oldValue传入cb执行cb回调方法。

如果cb没值,那就是watchEffect方法。直接去执行effect的run方法,将传入的source转换为getter,执行就好了。

总的来说 watch vs watchEffect

watch 用于需要知道是哪个数据项发生变化以及需要比较旧值和新值的情况,因为它提供了旧值和新值的参数。而 watchEffect 则更适合在不关心旧值和具体数据项变化的情况下使用,因为它只关注副作用函数的执行,不提供旧值和新值的参数。

 看到这里,如果你已经稍微理解watch和watchEffect的区别了,请一键三连哦~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OSPF-路由过滤、OSPFv3

OSPF——过滤,故障排除 OSPFv2——路由过滤 1、路由引入的过滤: import-route static route-policy //用于实现路由引入的过滤 filter-policy export,针对本区域传出的路由执行过滤 filter-policy import,针对传入本区域的路…

STM32学习和实践笔记(4): 分析和理解GPIO_InitTypeDef GPIO_InitStructure (a)

深入分析及学习一下上面这一段代码的构成与含义。 首先,这个GPIO_InitTypeDef GPIO_InitStructure;其实与int a 是完全类似的语法格式以及含义。 GPIO_InitStructure就相当于a这样一个变量。不过从这个变量的名字可以知道,这是一个用于GPIO初始化的结构…

部署项目遇到的各种问题总结

文章目录 前言一、后端问题 jar包运行出现错误宝塔面板使用jdk17二、数据库问题 版本问题三、前端问题 连不上后端总结 前言 在做完项目之后,为了让别人访问到自己的网站,就需要部署前端后端以及数据库,但是在部署的过程中出现了各种问题和困…

AcWing 786. 第k个数——算法基础课题解

AcWing 786. 第k个数 题目描述 给定一个长度为 n的整数数列,以及一个整数 k,请用快速选择算法求出数列从小到大排序后的第 k 个数。 输入格式 第一行包含两个整数 n 和 k。 第二行包含 n 个整数(所有整数均在 1∼10^9 范围内&#xff09…

用于无人机小型化设计的高精度温补晶振

用于无人机小型化设计的高精度温补晶振:TG2016SMN和TG2520SMN。无人机的发展可以说是非常的迅速,在安防,农业,交通,电力,直播等领域经常能看到无人机大显身手。无人机的应用场最是非常的广泛,功能更强&…

[蓝桥杯练习]通电

kruskal做法(加边) #include <bits/stdc.h> using namespace std; int x[10005],y[10005],z[10005];//存储i点的x与y坐标 int bcj[10005];//并查集 struct Edge{//边 int v1,v2; double w; }edge[2000005]; int cmp(Edge a, Edge b){return a.w < b.w;} int find(i…

数控加工4轴初探

4轴加工之前一直觉得很神秘&#xff0c;最近画了些时间研究了一下&#xff0c;做过之后发现起始也不是特别复杂。 图中是两步&#xff0c;一步是粗开&#xff0c;已不是用指形铣刀精加工螺旋槽。

百度地图 JavaScript API GL快速升级 和mapV的使用

一、百度地图 JavaScript API GL快速升级 JavaScript API GL升级指南 1、在原来的链接中添加type webgl 2、批量替换页面中的BMap直接替换为BMapGL 二、mapV的使用 MapVGL mapVGL的效率确实要快很多&#xff0c;一万个点基本实现秒现 1、加载mapvgl.min.js export const…

【群晖】部署UptimeKuma监控服务

【群晖】部署UptimeKuma监控服务 点击标题查看原文 本文讲解在群晖系统中使用docker方式部署UptimeKuma服务并通过外网地址正确访问 配置及版本 DSM&#xff1a;7.2&#xff08;7.x以上均可&#xff09; UptimeKuma&#xff1a;louislam/uptime-kuma:latest 安装 docker中下…

webapi 允许跨域

1.在Nuget安装webapi.cors 添加完会有这个包 然后在项目App_Start 目录下的WebApiConfig.cs里面添加 // Web API 配置和服务// 添加跨域设置config.EnableCors(new EnableCorsAttribute("*", "*", "*"));

鸿蒙分布式音乐播放-如何完成播放、暂停、上一曲、下一曲功能

介绍 本示例使用fileIo获取指定音频文件&#xff0c;并通过AudioPlayer完成了音乐的播放完成了基本的音乐播放、暂停、上一曲、下一曲功能&#xff1b;并使用DeviceManager完成了分布式设备列表的显示和分布式能力完成了音乐播放状态的跨设备分享。 本示例用到了与用户进行交…

[计算机效率] 磁盘空间分析工具:FolderSize

3.15 磁盘空间分析工具&#xff1a;FolderSize FolderSize是一款磁盘管理工具&#xff0c;提供预约交互式磁盘空间分析体验&#xff0c;可以可视化观察磁盘空间使用情况。程序可以帮助用户快速查看并统计硬盘中的各个分区所占用的空间大小以及文件夹和文件的大小&#xff0c;并…

跨境电商新纪元:独立站系统,让您的品牌飞跃国际舞台

在全球化的大潮中&#xff0c;跨境电商已不再是新鲜事物&#xff0c;而是众多企业实现品牌国际化、拓展市场份额的重要战略手段。然而&#xff0c;如何在竞争激烈的国际市场中脱颖而出&#xff0c;成为众多跨境电商企业面临的难题。今天&#xff0c;我要为大家介绍的&#xff0…

docker配置github仓库ghcr国内镜像加速

文章目录 说明ghcr.io简介配置镜像命令地址命令行方式1panel面板方式方式一&#xff1a;配置镜像加速&#xff0c;命令行拉取方式二&#xff1a;配置镜像仓库&#xff0c;可视化拉取 说明 由于使用的容器需要从github下载镜像&#xff0c;服务器在国外下载速度很慢&#xff0c…

CTF下加载CTFtraining题库以管理员身份导入 [HCTF 2018]WarmUp,之后以参赛者身份完成解题全过程

-------------------搭建CTFd------------------------------ 给大家介绍一个本地搭建比较好用的CTF比赛平台&#xff1a;CTFD。 CTFd是一个Capture The Flag框架&#xff0c;侧重于易用性和可定制性。它提供了运行CTF所需的一切&#xff0c;并且可以使用插件和主题轻松进行自…

二分图、匈牙利算法

目录 一&#xff0c;二分图 CodeForces 687A NP-Hard Problem 力扣 785. 判断二分图 二&#xff0c;完全二分图 1&#xff0c;完全二分图 2&#xff0c;K2,3 3&#xff0c;K3,3 三&#xff0c;匈牙利算法 1&#xff0c;二分图最大匹配 2&#xff0c;其他图论问题 一&…

交易所上币:区块链项目上线交易所流程

一、了解交易所/申请上币 在区块链项目上线交易所之前,首先需要对交易所进行充分的了解,包括交易所的基本信息、交易规则、飞BTC5186上币标准等。还需要了解交易所的申请上币流程,以便为后续的操作做好准备。 1.1 选择合适的交易所 在众多的交易所中 飞(BTC5186),如何选择一个…

NineData云原生智能数据管理平台新功能发布|2024年3月版

数据库 DevOps - 大功能升级 SQL 开发早期主要提供 SQL 窗口&#xff08;IDE&#xff09;功能&#xff0c;在产品经过将近两年时间的打磨&#xff0c;新增了大量的企业级功能&#xff0c;已经服务了上万开发者&#xff0c;覆盖了数据库设计、开发、测试、变更等生命周期的功能…

EVM Layer2 主流解决方案

深度解析主流 EVM Layer 2 解决方案&#xff1a;zk Rollups 和 Optimistic Rollups 随着以太坊网络的不断演进和 DeFi 生态系统的迅速增长&#xff0c;以太坊 Layer 2 解决方案日益受到关注。 其中&#xff0c;zk Rollups 和 Optimistic Rollups 作为两种备受瞩目的主流 EVM&…

Springboot自动获取接口实现

ServiceLoader加载接口实现步骤 1.编写接口 public interface CommunicationAdapterFactory {void setKernel(LocalKernel kernel);boolean providesAdapterFor(Vehicle vehicle);BasicCommunicationAdapter getAdapterFor(Vehicle vehicle); }2.编写实现 // 实现类 1 publi…