首页 文章 精选 留言 我的

精选列表

搜索[HTTP/3],共10000篇文章
优秀的个人博客,低调大师

Vue 3 中 watch 与 watchEffect 的依赖收集差异导致的一个隐蔽 bug

在一个后台管理项目里,我需要监听筛选条件的变化然后重新请求表格数据。当时用了 `watch` 监听一个由多个 `ref` 拼成的对象,结果发现修改其中某个 `ref` 时回调死活不触发。排查后发现是对 `watch` 和 `watchEffect` 依赖收集机制的理解有偏差:`watch` 默认只监听传入的响应式源本身,而 `watchEffect` 会自动追踪回调里用到的所有响应式依赖。这个差异在组合式 API 里很容易踩坑,尤其是把多个响应式数据组装成新对象再监听的时候。下面从实际代码出发,梳理两者的依赖收集规则、`deep` 和 `immediate` 的配合方式,以及几种常见的规避写法。 ## 问题复现 假设页面有三个筛选字段:关键词、状态、日期范围,都用 `ref` 管理。我最初写成了这样: ```ts import { ref, watch } from 'vue' const keyword = ref('') const status = ref('all') const dateRange = ref(null) const filters = { keyword: keyword.value, status: status.value, dateRange: dateRange.value, } watch(filters, (val) => { console.log('filters changed', val) fetchList(val) }) ``` 修改 `keyword.value` 时回调完全没反应。原因是 `filters` 只是一个普通对象,它在创建时就把各个 `ref` 的当前值取了出来,本身不具备响应性。`watch` 拿到的是一个普通对象,自然没法追踪任何变化。 即使加上 `deep: true` 也没用,因为 `deep` 只对响应式对象(`reactive` 或 `ref` 包裹的对象)内部的属性遍历有效,普通对象在传入的那一刻就已经和 `ref` 脱钩了。 ## watch 的依赖来源 `watch` 的响应式来源只有三种合法形式: - 一个 `ref`(包括 `computed`) - 一个 `reactive` 对象 - 一个 getter 函数 当传入 getter 时,Vue 会在内部执行这个 getter 并追踪期间访问到的响应式属性,这一点和 `watchEffect` 类似。区别在于 `watch` 只追踪 getter 返回的那个值所依赖的响应式源,不会追踪 getter 里访问但没参与返回值的那些。 所以正确写法是把要监听的数据组织进 getter 或 `reactive` 对象: ```ts // 方式 A:getter 返回新对象 watch( () => ({ keyword: keyword.value, status: status.value, dateRange: dateRange.value, }), (val) => { fetchList(val) } ) // 方式 B:用 reactive 承载 const filters = reactive({ keyword: '', status: 'all', dateRange: null as [string, string] | null, }) watch(filters, (val) => { fetchList(val) }) ``` 方式 A 里 getter 每次执行都会读取三个 `ref`,Vue 就能把它们登记为依赖。这里返回的是新对象,新旧值引用不同,所以回调每次都会触发,不需要 `deep`。但如果 getter 返回的是同一个对象引用(比如返回一个已经存在的 `reactive`),那就得看内部属性有没有变,这时才需要 `deep: true`。 方式 B 直接用 `reactive`,`watch` 默认就是浅层监听,对象属性变化不会触发,除非加 `deep: true`。不过 Vue 3.5 之后 `reactive` 对象的 `watch` 默认变成了深层,如果项目锁在 3.4 及以前,行为会不一样,这点在升级时容易忽略。 ## watchEffect 的自动收集 `watchEffect` 会在首次执行回调时同步追踪所有被读取的响应式属性。上面那段逻辑换成 `watchEffect` 就能正常工作: ```ts watchEffect(() => { const params = { keyword: keyword.value, status: status.value, dateRange: dateRange.value, } fetchList(params) }) ``` 只要回调里读了哪个 `ref`,它就被登记成依赖,任一变化都会重新执行。代价是依赖关系不够直观,回调里顺带读了别的响应式数据也会被一起追踪,容易造成意料之外的重复请求。 ## 实际项目里的选择 筛选条件变化触发请求这种场景,我更倾向用 `watch` 加 getter,因为依赖边界清晰,回调里能拿到新旧值做对比,也方便加防抖: ```ts import { watch, ref } from 'vue' import { debounce } from 'lodash-es' const keyword = ref('') const status = ref('all') const fetchList = debounce(async (params) => { const res = await api.list(params) // ... }, 300) watch( () => [keyword.value, status.value] as const, ([kw, st]) => { fetchList({ keyword: kw, status: st }) }, { immediate: true } ) ``` 返回数组比返回对象更轻,省去每次新建对象的开销;`as const` 是为了让 TS 推断出元组类型,回调参数解构时类型不会丢。 `watchEffect` 更适合依赖关系简单、不需要旧值、也不需要控制执行时机的场景,比如把某个 `props` 同步到本地状态、或者在依赖变化时写 localStorage。函数式组合里这种同步副作用用起来确实顺手。 ## 清理副作用的时机 `watch` 和 `watchEffect` 都支持在回调里注册清理函数,用于取消上一次未完成的请求: ```ts watch(keyword, async (kw, _, onCleanup) => { const controller = new AbortController() onCleanup(() => controller.abort()) const res = await fetch(`/api/search?q=${kw}`, { signal: controller.signal, }) // ... }) ``` 回调的第三个参数 `onCleanup` 会在下一次回调执行前、以及组件卸载时被调用。如果用了防抖,清理逻辑要写在防抖函数内部或者配合 `flush: 'post'`,否则 abort 的时机和实际发请求的时机对不上。 踩过这个普通对象当响应式源的坑之后,我现在写 `watch` 之前都会先确认监听目标到底是 `ref`、`reactive` 还是 getter,普通对象一律不直接传进去。

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

用户登录
用户注册