2.4 watch 监听变化

阶段1:侦探数字

目标:监听数字变化

操作

js 复制代码
// 导包
import { ref, watch } from 'vue'
const num = ref(0)
const info = ref('没变')
// 监听 num,变了就改 info
watch(num, () => info.value = '变了')

模板(<template>)里写

html 复制代码
{{ num }} {{ info }}
<button @click="num++">点我</button>

结果: 点击按钮,数字变大,后面显示"变了"。

错误示例

js 复制代码
// 错误:没有导包
watch(num, () => {})
// 后果:白屏,报错 watch is not defined

傻瓜口令:"watch 盯住 数字"


阶段2:侦探文字

目标:监听文字变化

操作

js 复制代码
import { ref, watch } from 'vue'
const str = ref('你好')
const info = ref('没变')
// 监听 str
watch(str, () => info.value = '变了')

模板(<template>)里写

html 复制代码
<input v-model="str">
{{ info }}

结果: 在输入框打字,下面文字变成"变了"。

错误示例

js 复制代码
// 错误:拼写错误
witch(str, () => {})
// 后果:报错 witch is not defined

傻瓜口令:"watch 盯住 文字"


阶段3:侦探开关

目标:监听开关变化

操作

js 复制代码
import { ref, watch } from 'vue'
const open = ref(false)
const info = ref('没变')
// 监听 open
watch(open, () => info.value = '变了')

模板(<template>)里写

html 复制代码
<button @click="open = !open">开关</button>
{{ info }}

结果: 点击开关按钮,下面文字变成"变了"。

错误示例

js 复制代码
// 错误:忘记写箭头函数
watch(open, info.value = '变了')
// 后果:页面一刷新就执行了一次,后面不执行

傻瓜口令:"watch 盯住 开关"


阶段4:拿到新的

目标:获取变化后的值

操作

js 复制代码
import { ref, watch } from 'vue'
const num = ref(0)
const info = ref('')
// 第一个参数是新值
watch(num, (newVal) => info.value = newVal)

模板(<template>)里写

html 复制代码
{{ num }} 最新是:{{ info }}
<button @click="num++">加1</button>

结果: 点击加1,"最新是:"后面显示当前的数字。

错误示例

js 复制代码
// 错误:参数写反了(其实watch不分反不反,但容易记混)
// 假设想拿旧值却只写了一个参数
watch(num, (oldVal) => ...) 
// 后果:拿到的其实是新值

傻瓜口令:"watch 拿到 新值"


阶段5:拿到旧的

目标:获取变化前的值

操作

js 复制代码
import { ref, watch } from 'vue'
const num = ref(0)
const info = ref('')
// 第二个参数是旧值
watch(num, (n, oldVal) => info.value = oldVal)

模板(<template>)里写

html 复制代码
{{ num }} 原来是:{{ info }}
<button @click="num++">加1</button>

结果: 点击加1,"原来是:"后面显示加1之前的数字。

错误示例

js 复制代码
// 错误:只写一个参数想拿旧值
watch(num, (oldVal) => info.value = oldVal)
// 后果:显示的是新值,不是旧值

傻瓜口令:"watch 拿到 旧值"


阶段6:盯错人了

目标:错误监听普通值

操作

js 复制代码
import { ref, watch } from 'vue'
let num = 0
// 错误:监听普通变量
watch(num, () => console.log('变了'))

模板(<template>)里写

html 复制代码
{{ num }}
<button @click="num++">点我</button>

结果: 点多少次按钮,都不会有反应。控制台可能会有黄色警告。

错误示例

js 复制代码
let a = 1
watch(a, () => {})
// 后果:Invalid watch source(无效的监听源)

傻瓜口令:"watch 不盯 普通数"


阶段7:只能盯表面

目标:默认不监听内部

操作

js 复制代码
// 导包
import { ref, watch } from 'vue'
const user = ref({ name: '小明' })
const info = ref('没变')
// 默认只盯 user 整体,不盯内部属性
watch(user, () => info.value = '变了')

模板(<template>)里写

html 复制代码
{{ user.name }} {{ info }}
<button @click="user.name = '小红'">改名字</button>
<button @click="user = { name: '大明' }">改整个人</button>

结果: 点击"改名字",没反应。点击"改整个人",变成了"变了"。

练习步骤

  1. 抄写上面的代码
  2. 点击两个按钮对比效果
  3. 记住默认看不到内部变化

错误示例

js 复制代码
// 错误:以为默认能听到内部
watch(user, () => {})
// 后果:改属性没反应,排查很久

傻瓜口令:"ref 只能 盯表面"


阶段8:开启深度游

目标:监听ref内部变化

操作

js 复制代码
import { ref, watch } from 'vue'
const user = ref({ name: '小明' })
const info = ref('没变')
// 第三个参数加上 { deep: true }
watch(user, () => info.value = '变了', { deep: true })

模板(<template>)里写

html 复制代码
{{ user.name }} {{ info }}
<button @click="user.name = '小红'">改名字</button>

结果: 点击"改名字",这次变成了"变了"。

练习步骤

  1. 抄写代码
  2. 加上 { deep: true }
  3. 再次测试修改名字

错误示例

js 复制代码
// 错误:deep 拼写错误
{ dep: true }
// 后果:配置无效,还是听不到

傻瓜口令:"开启 deep 盯深处"


阶段9:天生千里眼

目标:reactive自动深度监听

操作

js 复制代码
import { reactive, watch, ref } from 'vue'
const user = reactive({ name: '小明' })
const info = ref('没变')
// reactive 不需要 deep,自动是深度的
watch(user, () => info.value = '变了')

模板(<template>)里写

html 复制代码
{{ user.name }} {{ info }}
<button @click="user.name = '小红'">改名字</button>

结果: 点击"改名字",直接变了。

练习步骤

  1. 把 ref 改成 reactive
  2. 去掉 { deep: true }
  3. 验证是否有效

错误示例

js 复制代码
// 错误:给 reactive 加 deep
watch(user, ..., { deep: true })
// 后果:虽然没错,但是是废话,浪费性能

傻瓜口令:"reactive 天生 深层"


阶段10:只盯一点点

目标:监听对象的某个属性

操作

js 复制代码
import { reactive, watch, ref } from 'vue'
const user = reactive({ name: '小明', age: 18 })
const info = ref('没变')
// 写成箭头函数,只盯 name
watch(() => user.name, () => info.value = '名字变了')

模板(<template>)里写

html 复制代码
姓名:{{ user.name }} 年龄:{{ user.age }} {{ info }}
<button @click="user.name = '小红'">改名</button>
<button @click="user.age++">改年龄</button>

结果: 点改可名,info 变了。点改年龄,info 不变。

练习步骤

  1. 抄写代码
  2. 尝试不写箭头函数直接写 user.name
  3. 观察报错(下一章 L3 会讲)

错误示例

js 复制代码
// 错误:直接写属性值
watch(user.name, ...)
// 后果:报错或无效,必须写成函数

傻瓜口令:"箭头函数 盯特写"


阶段11:一上来就干

目标:立即执行监听

操作

js 复制代码
import { ref, watch } from 'vue'
const count = ref(0)
const info = ref('初始')
// 加上 immediate: true
watch(count, (n) => info.value = `数字是${n}`, { immediate: true })

模板(<template>)里写

html 复制代码
{{ info }}

结果: 页面一刷新,直接显示"数字是0",不需要等数据变。

练习步骤

  1. 抄写代码
  2. 去掉 immediate: true 对比效果
  3. 加上它,观察初始渲染

错误示例

js 复制代码
// 错误:拼写错误
{ immediate: trun }
// 后果:无效

傻瓜口令:"开启 immediate 马上做"


阶段12:同时盯两个

目标:监听多个数据

操作

js 复制代码
import { ref, watch } from 'vue'
const a = ref(0)
const b = ref(0)
// 用数组包起来
watch([a, b], () => console.log('有一个变了'))

模板(<template>)里写

html 复制代码
A: {{ a }} B: {{ b }}
<button @click="a++">改A</button>
<button @click="b++">改B</button>

结果: 无论点哪个按钮,控制台都会打印。

练习步骤

  1. 定义两个 ref
  2. 用数组 [] 包裹放在 watch 第一个参数
  3. 测试两个按钮

错误示例

js 复制代码
// 错误:没用数组包起来
watch(a, b, () => ...)
// 后果:参数错乱,报错

傻瓜口令:"数组包裹 盯多个"


阶段13:停止不盯了

目标:停止监听

操作

js 复制代码
import { ref, watch } from 'vue'
const count = ref(0)
const info = ref('正在监听')
// watch 会返回一个停止函数
const stop = watch(count, () => info.value = '变了')

const stopWatch = () => {
  stop() // 调用它就停止了
  info.value = '已停止'
}

模板(<template>)里写

html 复制代码
{{ count }} {{ info }}
<button @click="count++">加1</button>
<button @click="stopWatch">停止监听</button>

结果: 一开始点加1,状态会变。点停止监听后,再点加1,状态不再变。

练习步骤

  1. 接收 watch 的返回值
  2. 创建一个按钮调用这个返回值
  3. 验证停止效果

错误示例

js 复制代码
// 错误:以为会自动停止
// 组件不销毁,它就一直盯着

傻瓜口令:"拿到 stop 停止盯"


阶段14:变了做判断

目标:结合逻辑判断

操作

js 复制代码
import { ref, watch } from 'vue'
const score = ref(0)
const result = ref('不及格')
// 在回调里写 if
watch(score, (n) => {
  if (n >= 60) result.value = '及格'
  else result.value = '不及格'
})

模板(<template>)里写

html 复制代码
分数:<input v-model.number="score">
结果:{{ result }}

结果: 输入 50 显示不及格,输入 70 显示及格。

练习步骤

  1. 抄写代码
  2. 修改判断条件(比如改为 90 分优秀)
  3. 观察结果变化

错误示例

js 复制代码
// 错误:逻辑写反了
if (n < 60) result.value = '及格'
// 后果:逻辑错误,老师打人

傻瓜口令:"数据变了 做判断"


阶段15:算结果 vs 干苦力

目标:理解两者区别

对比表格

场景 写法A (Computed) 写法B (Watch)
用途 就算出一个新结果 不算结果,去执行动作
代码 computed(() => ...) watch(src, () => ...)

操作

js 复制代码
// 导包
import { ref, computed, watch } from 'vue'
const num = ref(1)
// 写法A:我是算命的,只算结果
const double = computed(() => num.value * 2)
// 写法B:我是干活的,只打印
watch(num, (n) => console.log('变了', n))

模板(<template>)里写

html 复制代码
{{ double }}
<button @click="num++">加1</button>

结果: 页面显示数字翻倍。控制台打印"变了"。

傻瓜口令:"computed 算结果 watch 去干活"


阶段16:要显示 vs 要动作

目标:场景选择

对比表格

场景 写法A (显示) 写法B (动作)
用途 页面要显示某个值 数据变了要弹窗/发请求
代码 用 Computed 用 Watch

操作

js 复制代码
import { ref, computed, watch } from 'vue'
const str = ref('hello')
// A:为了显示变大写
const upper = computed(() => str.value.toUpperCase())
// B:为了弹窗骚扰用户
watch(str, () => alert('你改了文字!'))

模板(<template>)里写

html 复制代码
<input v-model="str">
<p>{{ upper }}</p>

结果: 输入文字,下面大写自动变(Computed功劳)。浏览器弹出警告框(Watch功劳)。

傻瓜口令:"要显示 用计算 要动作 用监听"


阶段17:手动深 vs 自动深

目标:ref与reactive的watch区别

对比表格

场景 写法A (Ref对象) 写法B (Reactive对象)
用途 监听 Ref 包裹的对象 监听 Reactive 包裹的对象
代码 需要 deep: true 不需要,由其自动深度

操作

js 复制代码
import { ref, reactive, watch } from 'vue'
const r1 = ref({ n: 1 })
const r2 = reactive({ n: 1 })
// A: Ref 必须加 deep
watch(r1, () => console.log('Ref变了'), { deep: true })
// B: Reactive 自动 deep
watch(r2, () => console.log('Reactive变了'))

模板(<template>)里写

html 复制代码
<button @click="r1.n++">改Ref内部</button>
<button @click="r2.n++">改Reactive内部</button>

结果: 两个按钮点击后,控制台都会打印。

傻瓜口令:"ref 加深度 reactive 天然深"


阶段18:盯整体 vs 盯局部

目标:监听对象 vs 监听属性

对比表格

场景 写法A (整体) 写法B (局部)
用途 任何属性变了都知道 只有这个特定属性变了才执行
代码 watch(user, ...) watch(() => user.name, ...)

操作

js 复制代码
import { reactive, watch } from 'vue'
const user = reactive({ name: '小明', age: 10 })
// A: 只要 user 动,我就动
watch(user, () => console.log('整体变了'))
// B: 只有名字动,我才动
watch(() => user.name, () => console.log('名字变了'))

模板(<template>)里写

html 复制代码
<button @click="user.age++">改年龄</button>
<button @click="user.name='红'">改名字</button>

结果: 改年龄:只有"整体变了"。 改名字:"整体变了"和"名字变了"都有。

傻瓜口令:"盯整体 写变量 盯局部 写函数"


阶段19:等变化 vs 马上做

目标:普通 vs 立即执行

对比表格

场景 写法A (普通) 写法B (Immediate)
用途 数据变了再执行 从一开始就执行一次,变了再执行
代码 默认配置 { immediate: true }

操作

js 复制代码
import { ref, watch } from 'vue'
const n = ref(0)
// A: 懒人,踢一脚才动
watch(n, () => console.log('A动了'))
// B: 勤快人,上来就先干活
watch(n, () => console.log('B动了'), { immediate: true })

模板(<template>)里写

html 复制代码
<button @click="n++">踢一脚</button>

结果: 刷新页面:打印 "B动了"。 点击按钮:打印 "A动了" 和 "B动了"。

傻瓜口令:"不急 等变化 加急 马上做"


阶段20:独行侠 vs 组团跑

目标:单个 vs 数组

对比表格

场景 写法A (单个) 写法B (数组)
用途 分开监听,逻辑不同 多个数据变了执行同一个逻辑
代码 写两个 watch 写一个 watch,传数组

操作

js 复制代码
import { ref, watch } from 'vue'
const a = ref(0)
const b = ref(0)
// A: 分家
watch(a, () => console.log('a变'))
watch(b, () => console.log('b变'))
// B: 组团
watch([a, b], () => console.log('a或b变'))

模板(<template>)里写

html 复制代码
<button @click="a++">改A</button>
<button @click="b++">改B</button>

结果: 改A:打印 "a变" 和 "a或b变"。 改B:打印 "b变" 和 "a或b变"。

傻瓜口令:"分开 这一行 数组 包一起"


阶段21:终极二选一

目标:总结选择心法

对比表格

场景 选谁?
如果你需要根据数据生成一个新的值 ✅ Computed
如果你需要监控数据变化去发请求/存缓存 ✅ Watch
如果你只需要页面显示 ✅ Computed
如果你只需要打印日志 ✅ Watch

操作

js 复制代码
import { ref, computed, watch } from 'vue'
const count = ref(0)
// 选 Computed
const msg = computed(() => '数字:' + count.value)
// 选 Watch
watch(count, () => console.log('记录日志'))

模板(<template>)里写

html 复制代码
{{ msg }}
<button @click="count++">加</button>

结果: 页面显示最新消息,控制台有日志。各司其职。

傻瓜口令:"计算 找computed 办事 找watch"


阶段22:禁止直接盯数值

目标:监听属性的正确姿势

错误场景:想监听 reactive 里的一个属性,结果报错。

错误代码

js 复制代码
const user = reactive({ count: 1 })
// ❌ 错误:user.count 是个数字 1,watch 不能盯着数字 1
watch(user.count, () => console.log('变了'))

错误后果 : 控制台黄色警告:Invalid watch source(无效的监听源),页面没反应。

正确代码

js 复制代码
// ✅ 正确:用函数包起来,返回这个值
watch(() => user.count, () => console.log('变了'))

解决方法 : 想监听对象的属性,必须写成 () => user.prop

傻瓜口令:"禁止 直接 盯数值"


阶段23:旧值也变了

目标:对象引用的坑

错误场景:监听对象时,想对比新旧值有什么不同。

错误代码

js 复制代码
const user = reactive({ age: 10 })
watch(user, (newVal, oldVal) => {
  // ❌ 两个打印出来是一样的,都是新值
  console.log(newVal.age === oldVal.age) // true
})

错误后果: 你拿不到修改之前的值,因为它们指向同一个对象内存。

正确代码

js 复制代码
// ✅ 正确:如果非要对比数字,监听具体属性
watch(() => user.age, (n, o) => console.log(n, o))

解决方法 : 监听对象本身时,别指望 oldVal 是旧的。

傻瓜口令:"引用类型 旧值相同"


阶段24:死亡螺旋

目标:避免无限循环

错误场景:监听到数据变了,又去修改数据,导致无限触发。

错误代码

js 复制代码
const count = ref(0)
watch(count, () => {
  // ❌ 听到变了又去加1,又触发变了,又加1...
  count.value++
})

错误后果 : 浏览器卡死,或者Vue报错 Maximum recursive updates exceeded

正确代码

js 复制代码
// ✅ 正确:不要在回调里修改正在监听的数据
watch(count, () => console.log('只看不动'))

解决方法: 管住手,别自己改自己。

傻瓜口令:"里面修改 死循环"


阶段25:深度太深

目标:性能陷阱

错误场景:给超级大的对象开启深度监听。

错误代码

js 复制代码
// 假设 data 有 1000 层,1万个属性
watch(bigData, () => {}, { deep: true })

错误后果: 修改其中一个小数字,Vue 要遍历整个大对象,页面卡顿。

正确代码

js 复制代码
// ✅ 正确:只监听你需要关心的那个小属性
watch(() => bigData.info.name, () => {})

解决方法: 按需监听,别贪大求全。

傻瓜口令:"大对象 别用 deep"


阶段26:看不到DOM

目标:DOM 更新时机

错误场景:数据变了,想马上获取界面上的新内容。

错误代码

js 复制代码
watch(count, () => {
  // ❌ 这时候界面还没更新呢
  console.log(document.getElementById('box').innerHTML)
})

错误后果: 打印出来的还是旧的内容。

正确代码

js 复制代码
// ✅ 正确:加上 flush: 'post',表示"界面更完之后"
watch(count, () => { ... }, { flush: 'post' })

解决方法 : 想看更新后的界面,加 flush: 'post'

傻瓜口令:"想看DOM 加 post"


阶段27:喜新厌旧

目标:清理副作用

错误场景:搜索框输入太快,第1次的请求比第2次还晚回来,结果覆盖了第2次。

错误代码

js 复制代码
watch(search, (kw) => {
  // 发请求... 如果网络卡,旧请求会覆盖新请求
  axios.get(kw)
})

错误后果: 显示的数据和搜索词在界面上对不上。

正确代码

js 复制代码
// ✅ 正确:使用 onCleanup
watch(search, (kw, o, onCleanup) => {
  let expired = false
  onCleanup(() => expired = true) // 新的来了,把旧的标记过期
  
  fetch(kw).then(() => {
    if (!expired) showData()
  })
})

解决方法 : 利用 onCleanup 只有在下次触发前执行的特性。

傻瓜口令:"请求太快 记得清理"


阶段28:自动停车

目标:理解生命周期绑定

对比表格

场景 什么时候停止监听?
在 setup() 里写 组件销毁时,自动停止 ✅
在 setTimeout 里写 只要不刷新页面,一直监听 ❌

错误代码

js 复制代码
setTimeout(() => {
  // 危险:这个监听器可能永远不会死
  watch(count, () => ...)
}, 1000)

正确代码

js 复制代码
// ✅ 始终在 setup 根作用域里写 watch
watch(count, () => ...)

解决方法: 不要在异步回调里创建 watch,除非你手动 stop。

傻瓜口令:"在组件里 自动停"


阶段29:避坑指南

目标:全局总结

特训总结

  1. 盯局部 :不要 watch(obj.n),要 watch(() => obj.n)
  2. 防死锁:不要在回调里改该数据
  3. 惜性能 :大对象不要无脑 deep
  4. 重时机 :想操作DOM要 post

最终口令

傻瓜口令:"避开 坑点 走坦途"


相关推荐
m0_471199632 小时前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
貂蝉空大2 小时前
vue-pdf-embed分页预览解决文字丢失问题
前端·vue.js·pdf
ss2732 小时前
RuoYi-App 本地启动教程
前端·javascript·vue.js
用户248257824812 小时前
vue3快速入门
vue.js
涵涵(互关)3 小时前
JavaScript 对大整数(超过 2^53 - 1)的精度丢失问题
java·javascript·vue.js
天府之绝4 小时前
uniapp 中使用uview表单验证时,自定义扩展的表单,在改变时无法触发表单验证处理;
开发语言·前端·javascript·vue.js·uni-app
xkxnq4 小时前
第二阶段:Vue 组件化开发(第 20天)
前端·javascript·vue.js
刘一说4 小时前
腾讯位置服务JavaScript API GL地图组件库深度解析:Vue生态中的地理空间可视化利器
javascript·vue.js·信息可视化·webgl·webgis
*才华有限公司*4 小时前
#从401到200:Spring Boot + Vue 静态资源访问全链路问题解决方案
vue.js·spring boot·后端