第 12 课:watch 和防抖到底该怎么用
这一课很关键。
因为很多 Vue 初学者学到 ref、computed 之后,
接下来最容易卡住的就是:
- 什么情况该用
watch - 什么情况不该用
watch - 为什么搜索输入常常要"停一下再执行"
所以这节课要解决的核心问题是:
当页面状态变化后,需要去做一个"副作用动作"时,Vue 里应该怎么组织 watch 和防抖?
先讲结论
你先记住一句最重要的话:
computed 负责算结果,watch 负责做副作用
这句话非常重要。
你可以先这样理解:
computed
适合做:
- 从已有状态推导新结果
- 不产生额外副作用
比如:
- 根据任务列表算出统计卡片
- 根据筛选条件算出过滤结果
watch
适合做:
- 当某个状态变化后,额外执行一个动作
- 这个动作通常会影响"状态之外的世界"
比如:
- 同步地址栏
- 发请求
- 写本地存储
- 打日志
- 启动定时器
这就是为什么这一课会落到:
useTaskFilterQuerySync.tsuseDebounceFn.ts
上面。
这次我们做了什么
这一轮新增了:
src/composables/useDebounceFn.tssrc/composables/__tests__/useDebounceFn.spec.tsdocs/12-watch-and-debounce.md
更新了:
src/composables/useTaskFilterQuerySync.tssrc/composables/__tests__/useTaskFilterQuerySync.spec.tssrc/views/TasksView.vuesrc/components/tasks/TaskFilterBar.vue
这说明这次不是只讲"什么叫防抖",
而是把它真的接进了任务页的关键字同步流程里。
先看这次的真实问题是什么
现在任务页的关键字输入,会同步到地址栏:
text
/tasks?keyword=Vue
如果没有防抖,那么用户每敲一个字,页面都会立刻执行一次:
ts
router.replace(...)
会带来几个问题:
1. 副作用太频繁
用户输入 Vue 三个字母,
可能就会触发 3 次地址栏同步。
2. 以后接真实请求时会更糟
现在我们只是同步 URL,
以后如果这里变成"根据关键字请求后端列表",
那就会变成每敲一个字都发一次请求。
3. 很难表现"正在等待稳定输入"
用户其实还没输入完,
系统却已经开始不停响应。
这时就很适合引入:
防抖
什么叫防抖
你可以把防抖先理解成一句很口语的话:
先别急着执行,等用户停一下再说
比如设置 400ms 防抖:
- 用户输入一个字
- 系统先不执行
- 如果 400ms 内又输入了新内容,就重新计时
- 只有用户停下来 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
作用:
地址栏变化时,把它回填进页面状态
第二组
监听:
keywordstatusFilterpriorityFilter
作用:
页面筛选变化时,把它写回地址栏
这里就能看出 watch 的价值:
它不是帮你"算值",
它是帮你"在值变化后执行动作"。
为什么状态筛选立即同步,而关键字要防抖
这次是一个很典型的"不同输入类型,不同副作用策略"。
状态、优先级下拉框
它们变化频率低,而且每次选择都很明确。
所以:
立即同步到 URL 更合理
关键字输入框
它变化频率很高,而且用户往往连续输入。
所以:
延迟一点再同步更合理
这就是为什么我们没有"全都防抖",
而是只把关键字同步做成防抖。
这一步非常值得你学,因为真实项目从来不是"一个规则打天下",
而是:
不同动作,根据输入特性采用不同策略
为什么这里还要处理"旧任务回写"
这是这一课最容易被忽略、但很重要的一点。
假设用户这样操作:
- 输入
V - 防抖开始等待
- 还没到 400ms,用户又跳转了地址或浏览器后退了
如果你不取消旧的防抖任务,
等时间一到,旧任务可能还会把旧关键字重新写回地址栏。
这就叫:
过期副作用回写
所以这次在地址栏变化时,我们会先执行:
debouncedQueryReplace.cancel()
这就是副作用控制里非常重要的一种思维:
不是只管触发,还要管旧任务失效
为什么现在筛选栏会显示"同步中"提示
这次 TaskFilterBar.vue 下面新增了一行提示。
它背后对应的就是:
isKeywordQuerySyncPendingkeywordQuerySyncDelay
这一步很适合教学,因为它能帮你把"看不见的内部状态"变成"看得见的界面反馈"。
你现在要开始有这个意识:
很多逻辑状态,如果对用户有意义,就值得被展示出来
比如:
- 加载中
- 保存中
- 同步中
- 校验中
这些都不是"内部细节",
它们其实都是交互的一部分。
为什么 computed 不适合替代这里的 watch
很多初学者看到"状态变化后要做事",
会下意识想:
能不能也用 computed?
这里不行。
因为 computed 的本质是:
返回一个根据依赖自动推导的新值
它不适合承担:
router.replacefetchlocalStorage.setItemconsole.log
这种副作用动作。
所以你一定要把这两个角色彻底分清:
computed是推导watch是响应变化后的动作
这次为什么要给防抖工具单独写测试
现在新增了:
它验证了:
- 连续快速调用时,只执行最后一次
cancel能取消挂起任务flush能立刻执行挂起任务
这很重要。
因为工具型 composable 一旦写对,
后面很多页面都能直接复用。
而且这种"时间相关逻辑",
如果没有测试,很容易在后面重构时悄悄出问题。
这次为什么继续给查询同步补测试
现在的 useTaskFilterQuerySync.spec.ts 里,多了一条专门测试:
只有关键字变化时,会延迟一段时间后再写回地址栏
这条测试的意义非常大。
因为它不是在测试"能不能同步",
而是在测试:
是不是按预期的节奏同步
这说明你开始在测试里不仅关心结果,
也关心行为过程了。
这正是前端工程能力进阶的重要信号。
真实项目里 watch 最常用的 6 种场景
你现在可以先把下面这些记住:
1. 同步 URL
比如这次的任务页筛选条件。
2. 发起请求
比如筛选条件变化后自动重新拉列表。
3. 写入本地存储
比如主题设置、表单草稿。
4. 启动或清理定时器
比如轮询、延迟提示、防抖、节流。
5. 触发表单联动副作用
比如国家变了,要重新加载省份列表。
6. 调试和埋点
比如记录用户筛选行为、输出开发日志。
这节课最容易犯的 8 个错误
1. 把 computed 和 watch 混用
推导值用 computed,副作用用 watch。
2. 一看到状态变化就无脑写 watch
如果你只是想得到一个新值,通常应该先考虑 computed。
3. 每次输入都立刻触发重副作用
比如请求、同步、校验,这通常都应该考虑防抖。
4. 只会防抖,不会取消旧任务
旧副作用可能在错误时机回写结果。
5. 不把挂起状态暴露出来
用户会感觉页面"怎么没反应",其实系统只是在等待输入稳定。
6. 所有筛选动作都强行套同一种同步策略
输入框和下拉框,通常不该完全一样对待。
7. 不测试时间相关逻辑
防抖、定时器、副作用节奏,都很容易在没有测试时变脆弱。
8. 把副作用写得四处分散
最好像这次一样,把"路由同步副作用"集中到一个明确的 composable 里。
你现在应该能回答的 10 个问题
computed和watch的职责差别到底是什么?- 为什么"当状态变化时去做事"通常应该想到
watch? - 什么叫防抖?
- 为什么关键字输入比状态下拉更需要防抖?
useDebounceFn这次暴露的 4 个能力分别是什么?- 为什么地址栏变化时要取消旧的关键字同步任务?
- 为什么
isPending值得暴露给界面层? - 为什么时间相关逻辑特别需要测试?
- 为什么这次没有把所有筛选动作都做成防抖?
- 以后如果把关键字搜索改成真实请求,为什么这套思路仍然成立?
这节课的动手练习
练习 1
打开 useDebounceFn.ts,自己口述一遍:
run干什么cancel干什么flush干什么isPending干什么
目的:
训练你把"工具型 composable"拆成几个明确责任去理解。
练习 2
手动打开任务页,快速连续输入关键字,观察:
- 列表筛选会不会立刻变化
- 地址栏会不会立刻变化
- "同步中"提示什么时候出现、什么时候消失
目的:
体会"页面即时反馈"和"副作用延迟执行"是可以同时存在的。
练习 3
试着自己思考下面这些动作,哪些该防抖,哪些不该:
- 用户点击"删除任务"
- 用户输入搜索关键字
- 用户切换状态下拉框
- 用户编辑昵称并自动保存
目的:
训练你判断"动作频率"和"副作用成本"。
这节课的复习结论
把这一课压缩成 8 句话:
computed用来推导值,watch用来响应变化并执行副作用。- 搜索输入、自动保存、地址栏同步这类高频副作用通常都值得考虑防抖。
- 防抖的核心不是"变慢",而是"等输入稳定后只执行最后一次"。
useDebounceFn把防抖执行、取消、立即执行和挂起状态统一封装了。- 关键字输入和下拉筛选的交互特性不同,所以同步策略也不应该完全一样。
- 副作用不仅要会触发,还要会取消过期任务。
isPending这类状态很适合转成界面提示,让用户知道系统正在等待稳定输入。- 时间相关逻辑如果没有测试,后续重构时最容易悄悄出错。