第 12 课:`watch` 和防抖到底该怎么用

第 12 课:watch 和防抖到底该怎么用

这一课很关键。

因为很多 Vue 初学者学到 refcomputed 之后,

接下来最容易卡住的就是:

  • 什么情况该用 watch
  • 什么情况不该用 watch
  • 为什么搜索输入常常要"停一下再执行"

所以这节课要解决的核心问题是:

当页面状态变化后,需要去做一个"副作用动作"时,Vue 里应该怎么组织 watch 和防抖?


先讲结论

你先记住一句最重要的话:

computed 负责算结果,watch 负责做副作用

这句话非常重要。

你可以先这样理解:

computed

适合做:

  • 从已有状态推导新结果
  • 不产生额外副作用

比如:

  • 根据任务列表算出统计卡片
  • 根据筛选条件算出过滤结果

watch

适合做:

  • 当某个状态变化后,额外执行一个动作
  • 这个动作通常会影响"状态之外的世界"

比如:

  • 同步地址栏
  • 发请求
  • 写本地存储
  • 打日志
  • 启动定时器

这就是为什么这一课会落到:

  • useTaskFilterQuerySync.ts
  • useDebounceFn.ts

上面。


这次我们做了什么

这一轮新增了:

  • src/composables/useDebounceFn.ts
  • src/composables/__tests__/useDebounceFn.spec.ts
  • docs/12-watch-and-debounce.md

更新了:

  • src/composables/useTaskFilterQuerySync.ts
  • src/composables/__tests__/useTaskFilterQuerySync.spec.ts
  • src/views/TasksView.vue
  • src/components/tasks/TaskFilterBar.vue

这说明这次不是只讲"什么叫防抖",

而是把它真的接进了任务页的关键字同步流程里。


先看这次的真实问题是什么

现在任务页的关键字输入,会同步到地址栏:

text 复制代码
/tasks?keyword=Vue

如果没有防抖,那么用户每敲一个字,页面都会立刻执行一次:

ts 复制代码
router.replace(...)

会带来几个问题:

1. 副作用太频繁

用户输入 Vue 三个字母,

可能就会触发 3 次地址栏同步。


2. 以后接真实请求时会更糟

现在我们只是同步 URL,

以后如果这里变成"根据关键字请求后端列表",

那就会变成每敲一个字都发一次请求。


3. 很难表现"正在等待稳定输入"

用户其实还没输入完,

系统却已经开始不停响应。

这时就很适合引入:

防抖


什么叫防抖

你可以把防抖先理解成一句很口语的话:

先别急着执行,等用户停一下再说

比如设置 400ms 防抖:

  1. 用户输入一个字
  2. 系统先不执行
  3. 如果 400ms 内又输入了新内容,就重新计时
  4. 只有用户停下来 400ms,才真正执行

这就是搜索框、自动保存、地址栏同步里最常见的节奏。


为什么这次没有把防抖写死在任务页里

这次我们专门新增了:

这是一个通用防抖工具。

它的作用不是"只给任务页用",

而是:

把"延迟执行最后一次动作"这件事抽成一个独立能力

这样你以后可以把它复用到很多地方:

  • 搜索请求
  • 表单自动保存
  • 地址栏同步
  • 远程校验
  • 输入联想

这也是一个很好的抽象练习。


useDebounceFn 这次解决了什么

它主要做了 4 件事:

1. run

注册一次新的延迟执行任务。


2. cancel

取消当前还没执行的任务。


3. flush

不再继续等,立刻执行当前那次挂起任务。


4. isPending

告诉页面:现在是不是还有一个没执行完的防抖任务。

这第四点非常适合做界面提示。

所以这次任务页筛选栏下方才会出现:

关键字输入已变化,系统会在 400ms 停顿后再同步到地址栏。

这不是装饰,而是把"防抖中的内部状态"可视化了。


为什么这次正好能用来讲 watch

因为当前这个场景非常典型:

我们不是想"算一个值"

而是想:

当 keyword / status / priority 变化时,去执行一个副作用:更新地址栏

这正是 watch 最擅长的事情。

所以你现在要开始形成一个判断标准:

如果你脑子里的需求句式是:

当 X 变化时,去做 Y

那你就应该优先想到:

watch


这次 watch 真正监听了什么

useTaskFilterQuerySync.ts 里,现在有两组监听。

第一组

监听:

ts 复制代码
route.query

作用:

地址栏变化时,把它回填进页面状态


第二组

监听:

  • keyword
  • statusFilter
  • priorityFilter

作用:

页面筛选变化时,把它写回地址栏

这里就能看出 watch 的价值:

它不是帮你"算值",

它是帮你"在值变化后执行动作"。


为什么状态筛选立即同步,而关键字要防抖

这次是一个很典型的"不同输入类型,不同副作用策略"。

状态、优先级下拉框

它们变化频率低,而且每次选择都很明确。

所以:

立即同步到 URL 更合理


关键字输入框

它变化频率很高,而且用户往往连续输入。

所以:

延迟一点再同步更合理

这就是为什么我们没有"全都防抖",

而是只把关键字同步做成防抖。

这一步非常值得你学,因为真实项目从来不是"一个规则打天下",

而是:

不同动作,根据输入特性采用不同策略


为什么这里还要处理"旧任务回写"

这是这一课最容易被忽略、但很重要的一点。

假设用户这样操作:

  1. 输入 V
  2. 防抖开始等待
  3. 还没到 400ms,用户又跳转了地址或浏览器后退了

如果你不取消旧的防抖任务,

等时间一到,旧任务可能还会把旧关键字重新写回地址栏。

这就叫:

过期副作用回写

所以这次在地址栏变化时,我们会先执行:

  • debouncedQueryReplace.cancel()

这就是副作用控制里非常重要的一种思维:

不是只管触发,还要管旧任务失效


为什么现在筛选栏会显示"同步中"提示

这次 TaskFilterBar.vue 下面新增了一行提示。

它背后对应的就是:

  • isKeywordQuerySyncPending
  • keywordQuerySyncDelay

这一步很适合教学,因为它能帮你把"看不见的内部状态"变成"看得见的界面反馈"。

你现在要开始有这个意识:

很多逻辑状态,如果对用户有意义,就值得被展示出来

比如:

  • 加载中
  • 保存中
  • 同步中
  • 校验中

这些都不是"内部细节",

它们其实都是交互的一部分。


为什么 computed 不适合替代这里的 watch

很多初学者看到"状态变化后要做事",

会下意识想:

能不能也用 computed?

这里不行。

因为 computed 的本质是:

返回一个根据依赖自动推导的新值

它不适合承担:

  • router.replace
  • fetch
  • localStorage.setItem
  • console.log

这种副作用动作。

所以你一定要把这两个角色彻底分清:

  • computed 是推导
  • watch 是响应变化后的动作

这次为什么要给防抖工具单独写测试

现在新增了:

它验证了:

  1. 连续快速调用时,只执行最后一次
  2. cancel 能取消挂起任务
  3. flush 能立刻执行挂起任务

这很重要。

因为工具型 composable 一旦写对,

后面很多页面都能直接复用。

而且这种"时间相关逻辑",

如果没有测试,很容易在后面重构时悄悄出问题。


这次为什么继续给查询同步补测试

现在的 useTaskFilterQuerySync.spec.ts 里,多了一条专门测试:

只有关键字变化时,会延迟一段时间后再写回地址栏

这条测试的意义非常大。

因为它不是在测试"能不能同步",

而是在测试:

是不是按预期的节奏同步

这说明你开始在测试里不仅关心结果,

也关心行为过程了。

这正是前端工程能力进阶的重要信号。


真实项目里 watch 最常用的 6 种场景

你现在可以先把下面这些记住:

1. 同步 URL

比如这次的任务页筛选条件。


2. 发起请求

比如筛选条件变化后自动重新拉列表。


3. 写入本地存储

比如主题设置、表单草稿。


4. 启动或清理定时器

比如轮询、延迟提示、防抖、节流。


5. 触发表单联动副作用

比如国家变了,要重新加载省份列表。


6. 调试和埋点

比如记录用户筛选行为、输出开发日志。


这节课最容易犯的 8 个错误

1. 把 computedwatch 混用

推导值用 computed,副作用用 watch


2. 一看到状态变化就无脑写 watch

如果你只是想得到一个新值,通常应该先考虑 computed


3. 每次输入都立刻触发重副作用

比如请求、同步、校验,这通常都应该考虑防抖。


4. 只会防抖,不会取消旧任务

旧副作用可能在错误时机回写结果。


5. 不把挂起状态暴露出来

用户会感觉页面"怎么没反应",其实系统只是在等待输入稳定。


6. 所有筛选动作都强行套同一种同步策略

输入框和下拉框,通常不该完全一样对待。


7. 不测试时间相关逻辑

防抖、定时器、副作用节奏,都很容易在没有测试时变脆弱。


8. 把副作用写得四处分散

最好像这次一样,把"路由同步副作用"集中到一个明确的 composable 里。


你现在应该能回答的 10 个问题

  1. computedwatch 的职责差别到底是什么?
  2. 为什么"当状态变化时去做事"通常应该想到 watch
  3. 什么叫防抖?
  4. 为什么关键字输入比状态下拉更需要防抖?
  5. useDebounceFn 这次暴露的 4 个能力分别是什么?
  6. 为什么地址栏变化时要取消旧的关键字同步任务?
  7. 为什么 isPending 值得暴露给界面层?
  8. 为什么时间相关逻辑特别需要测试?
  9. 为什么这次没有把所有筛选动作都做成防抖?
  10. 以后如果把关键字搜索改成真实请求,为什么这套思路仍然成立?

这节课的动手练习

练习 1

打开 useDebounceFn.ts,自己口述一遍:

  • run 干什么
  • cancel 干什么
  • flush 干什么
  • isPending 干什么

目的:

训练你把"工具型 composable"拆成几个明确责任去理解。


练习 2

手动打开任务页,快速连续输入关键字,观察:

  • 列表筛选会不会立刻变化
  • 地址栏会不会立刻变化
  • "同步中"提示什么时候出现、什么时候消失

目的:

体会"页面即时反馈"和"副作用延迟执行"是可以同时存在的。


练习 3

试着自己思考下面这些动作,哪些该防抖,哪些不该:

  1. 用户点击"删除任务"
  2. 用户输入搜索关键字
  3. 用户切换状态下拉框
  4. 用户编辑昵称并自动保存

目的:

训练你判断"动作频率"和"副作用成本"。


这节课的复习结论

把这一课压缩成 8 句话:

  1. computed 用来推导值,watch 用来响应变化并执行副作用。
  2. 搜索输入、自动保存、地址栏同步这类高频副作用通常都值得考虑防抖。
  3. 防抖的核心不是"变慢",而是"等输入稳定后只执行最后一次"。
  4. useDebounceFn 把防抖执行、取消、立即执行和挂起状态统一封装了。
  5. 关键字输入和下拉筛选的交互特性不同,所以同步策略也不应该完全一样。
  6. 副作用不仅要会触发,还要会取消过期任务。
  7. isPending 这类状态很适合转成界面提示,让用户知道系统正在等待稳定输入。
  8. 时间相关逻辑如果没有测试,后续重构时最容易悄悄出错。
相关推荐
鹏程十八少2 小时前
2.2026金三银四 Android Handler 完全指南:28道高频面试题 + 源码解析 + 图解 (一文通关)
android·前端·面试
大连好光景2 小时前
Fiddler、Wireshark、Charles三种抓包工具的对比
前端·fiddler·wireshark
gyx_这个杀手不太冷静2 小时前
大人工智能时代下前端界面全新开发模式的思考(五)
前端·架构·ai编程
im_AMBER2 小时前
Leetcode 158 数组中的第K个最大元素 | 查找和最小的 K 对数字
javascript·数据结构·算法·leetcode·
bug修复工程师2 小时前
Vue 项目高德地图性能优化实战:从卡死到丝滑的完整过程
vue.js
qq_12084093712 小时前
Three.js 场景性能优化实战:首屏、帧率与内存的工程化治理
开发语言·javascript·性能优化·three.js
Ruihong2 小时前
Vue v-if 转 React:VuReact 怎么处理?
vue.js·react.js·面试
qiao若huan喜2 小时前
12、webgl 基本概念 +满天星星眨眼睛
前端·信息可视化·webgl
陆枫Larry2 小时前
搞懂 package.json 和 package-lock.json
前端