Vue3中Watch与watchEffect的核心差异及适用场景是什么?
一、Vue3 侦听器(Watch)核心概念与基本示例
1.1 什么是侦听器?
在Vue3中,侦听器(Watch)是一种强大的工具,用于在响应式状态变化时执行副作用操作。当你需要在数据变化时执行异步操作、更新DOM或者修改其他状态时,侦听器就派上用场了。它与计算属性不同,计算属性主要用于声明式地计算衍生值,而侦听器更适合处理副作用逻辑。
1.2 基本示例(选项式API)
export default {
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)',
loading: false
}
},
watch: {
// 每当 question 改变时,这个函数就会执行
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer()
}
}
},
methods: {
async getAnswer() {
this.loading = true
this.answer = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
this.answer = (await res.json()).answer
} catch (error) {
this.answer = 'Error! Could not reach the API. ' + error
} finally {
this.loading = false
}
}
}
}
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
1.3 基本示例(组合式API)
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
1.4 侦听数据源类型
watch的第一个参数可以是不同形式的数据源:
- 单个ref(包括计算属性)
- 响应式对象
- getter函数
- 多个数据源组成的数组
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
二、深层侦听器
2.1 什么是深层侦听器?
watch默认是浅层的:被侦听的属性仅在被赋新值时才会触发回调函数,而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要使用深层侦听器。
2.2 深层侦听器示例(选项式API)
export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// 注意:在嵌套的变更中,只要没有替换对象本身,newValue和oldValue相同
},
deep: true
}
}
}
2.3 深层侦听器示例(组合式API)
const obj = reactive({ count: 0 })
// 直接传入响应式对象,隐式创建深层侦听器
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:newValue和oldValue是相等的,因为它们是同一个对象!
})
obj.count++
2.4 注意事项
深度侦听需要遍历被侦听对象中的所有嵌套属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。在Vue 3.5+中,deep选项还可以是一个数字,表示最大遍历深度。
三、即时回调的侦听器
3.1 什么是即时回调的侦听器?
watch默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时立即执行一遍回调,比如请求初始数据。
3.2 即时回调示例(选项式API)
export default {
watch: {
question: {
handler(newQuestion) {
// 在组件实例创建时会立即调用
},
// 强制立即执行回调
immediate: true
}
}
}
3.3 即时回调示例(组合式API)
watch(
source,
(newValue, oldValue) => {
// 立即执行,且当source改变时再次执行
},
{ immediate: true }
)
四、一次性侦听器
4.1 什么是一次性侦听器?
一次性侦听器仅在被侦听源第一次变化时触发回调,之后自动停止侦听。这个特性在Vue 3.4及以上版本支持。
往期文章归档
- Vue 3自定义指令如何赋能表单自动聚焦与防抖输入的高效实现?
- Vue3中如何优雅实现支持多绑定变量和修饰符的双向绑定组件?
- Vue 3表单验证如何从基础规则到异步交互构建完整验证体系?
- Vue3响应式系统如何支撑表单数据的集中管理、动态扩展与实时计算?
- Vue3跨组件通信中,全局事件总线与provide/inject该如何正确选择?
- Vue3表单事件处理:v-model如何实现数据绑定、验证与提交?
- Vue应用如何基于DOM事件传播机制与事件修饰符实现高效事件处理?
- Vue3中如何在调用事件处理函数时同时传递自定义参数和原生DOM事件?参数顺序有哪些注意事项?
- 从捕获到冒泡:Vue事件修饰符如何重塑事件执行顺序?
- Vue事件处理:内联还是方法事件处理器,该如何抉择?
- Vue事件绑定中v-on与@语法如何取舍?参数传递与原生事件处理有哪些实战技巧?
- Vue 3中列表排序时为何必须复制数组而非直接修改原始数据?
- Vue虚拟滚动如何将列表DOM数量从万级降至十位数?
- Vue3中v-if与v-for直接混用为何会报错?计算属性如何解决优先级冲突?
- 为何在Vue3递归组件中必须用v-if判断子项存在?
- Vue3列表渲染中,如何用数组方法与计算属性优化v-for的数据处理?
- Vue v-for的key:为什么它能解决列表渲染中的“玄学错误”?选错会有哪些后果?
- Vue3中v-for与v-if为何不能直接共存于同一元素?
- Vue3中v-if与v-show的本质区别及动态组件状态保持的关键策略是什么?
- Vue3中v-show如何通过CSS修改display属性控制条件显示?与v-if的应用场景该如何区分?
- Vue3条件渲染中v-if系列指令如何合理使用与规避错误?
- Vue3动态样式控制:ref、reactive、watch与computed的应用场景与区别是什么?
- Vue3中动态样式数组的后项覆盖规则如何与计算属性结合实现复杂状态样式管理?
- Vue浅响应式如何解决深层响应式的性能问题?适用场景有哪些? - cmdragon's Blog
- Vue 3组合式API中ref与reactive的核心响应式差异及使用最佳实践是什么? - cmdragon's Blog
- Vue 3组合式API中ref与reactive的核心响应式差异及使用最佳实践是什么? - cmdragon's Blog
- Vue3响应式系统中,对象新增属性、数组改索引、原始值代理的问题如何解决? - cmdragon's Blog
- Vue 3中watch侦听器的正确使用姿势你掌握了吗?深度监听、与watchEffect的差异及常见报错解析 - cmdragon's Blog
- Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗 - cmdragon's Blog
- Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗 - cmdragon's Blog
- 为什么Vue 3需要ref函数?它的响应式原理与正确用法是什么? - cmdragon's Blog
- Vue 3中reactive函数如何通过Proxy实现响应式?使用时要避开哪些误区? - cmdragon's Blog
- Vue3响应式系统的底层原理与实践要点你真的懂吗? - cmdragon's Blog
- Vue 3模板如何通过编译三阶段实现从声明式语法到高效渲染的跨越 - cmdragon's Blog
- 快速入门Vue模板引用:从收DOM“快递”到调子组件方法,你玩明白了吗? - cmdragon's Blog
- 快速入门Vue模板里的JS表达式有啥不能碰?计算属性为啥比方法更能打? - cmdragon's Blog
- 快速入门Vue的v-model表单绑定:语法糖、动态值、修饰符的小技巧你都掌握了吗? - cmdragon's Blog
- 快速入门Vue3事件处理的挑战题:v-on、修饰符、自定义事件你能通关吗? - cmdragon's Blog
- 快速入门Vue3的v-指令:数据和DOM的“翻译官”到底有多少本事? - cmdragon's Blog
- 快速入门Vue3,插值、动态绑定和避坑技巧你都搞懂了吗? - cmdragon's Blog
- 想让PostgreSQL快到飞起?先找健康密码还是先换引擎? - cmdragon's Blog
- 想让PostgreSQL查询快到飞起?分区表、物化视图、并行查询这三招灵不灵? - cmdragon's Blog
- 子查询总拖慢查询?把它变成连接就能解决? - cmdragon's Blog
- PostgreSQL全表扫描慢到崩溃?建索引+改查询+更统计信息三招能破? - cmdragon's Blog
- 复杂查询总拖后腿?PostgreSQL多列索引+覆盖索引的神仙技巧你get没? - cmdragon's Blog
- 只给表子集建索引?用函数结果建索引?PostgreSQL这俩操作凭啥能省空间又加速? - cmdragon's Blog
- B-tree索引像字典查词一样工作?那哪些数据库查询它能加速,哪些不能? - cmdragon's Blog
- 想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷? - cmdragon's Blog
- PostgreSQL的“时光机”MVCC和锁机制是怎么搞定高并发的? - cmdragon's Blog
- PostgreSQL性能暴涨的关键?内存IO并发参数居然要这么设置? - cmdragon's Blog
- 大表查询慢到翻遍整个书架?PostgreSQL分区表教你怎么“分类”才高效
- PostgreSQL 查询慢?是不是忘了优化 GROUP BY、ORDER BY 和窗口函数? - cmdragon's Blog
- PostgreSQL里的子查询和CTE居然在性能上“掐架”?到底该站哪边? - cmdragon's Blog
- PostgreSQL选Join策略有啥小九九?Nested Loop/Merge/Hash谁是它的菜? - cmdragon's Blog
- PostgreSQL新手SQL总翻车?这7个性能陷阱你踩过没? - cmdragon's Blog
- PostgreSQL索引选B-Tree还是GiST?“瑞士军刀”和“多面手”的差别你居然还不知道? - cmdragon's Blog
- 想知道数据库怎么给查询“算成本选路线”?EXPLAIN能帮你看明白? - cmdragon's Blog
- PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机? - cmdragon's Blog
- PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前? - cmdragon's Blog
- 转账不翻车、并发不干扰,PostgreSQL的ACID特性到底有啥魔法? - cmdragon's Blog
- 银行转账不白扣钱、电商下单不超卖,PostgreSQL事务的诀窍是啥? - cmdragon's Blog
- PostgreSQL里的PL/pgSQL到底是啥?能让SQL从“说目标”变“讲步骤”? - cmdragon's Blog
- PostgreSQL视图不存数据?那它怎么简化查询还能递归生成序列和控制权限? - cmdragon's Blog
- PostgreSQL索引这么玩,才能让你的查询真的“飞”起来? - cmdragon's Blog
- PostgreSQL的表关系和约束,咋帮你搞定用户订单不混乱、学生选课不重复? - cmdragon's Blog
- PostgreSQL查询的筛子、排序、聚合、分组?你会用它们搞定数据吗? - cmdragon's Blog
- PostgreSQL数据类型怎么选才高效不踩坑? - cmdragon's Blog
- 想解锁PostgreSQL查询从基础到进阶的核心知识点?你都get了吗? - cmdragon's Blog
- PostgreSQL DELETE居然有这些操作?返回数据、连表删你试过没? - cmdragon's Blog
- PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
- PostgreSQL插入数据还在逐条敲?批量、冲突处理、返回自增ID的技巧你会吗? - cmdragon's Blog
- PostgreSQL的“仓库-房间-货架”游戏,你能建出电商数据库和表吗? - cmdragon's Blog
- PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
- 能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
- 给接口加新字段又不搞崩老客户端?FastAPI的多版本API靠哪三招实现? - cmdragon's Blog
- 流量突增要搞崩FastAPI?熔断测试是怎么防系统雪崩的? - cmdragon's Blog
- FastAPI秒杀库存总变负数?Redis分布式锁能帮你守住底线吗 - cmdragon's Blog
- FastAPI的CI流水线怎么自动测端点,还能让Allure报告美到犯规? - cmdragon's Blog
- 如何用GitHub Actions为FastAPI项目打造自动化测试流水线? - cmdragon's Blog
免费好用的热门在线工具
- 文件格式转换器 - 应用商店 | By cmdragon
- M3U8在线播放器 - 应用商店 | By cmdragon
- 快图设计 - 应用商店 | By cmdragon
- 高级文字转图片转换器 - 应用商店 | By cmdragon
- RAID 计算器 - 应用商店 | By cmdragon
- 在线PS - 应用商店 | By cmdragon
- Mermaid 在线编辑器 - 应用商店 | By cmdragon
- 数学求解计算器 - 应用商店 | By cmdragon
- 智能提词器 - 应用商店 | By cmdragon
- 魔法简历 - 应用商店 | By cmdragon
- Image Puzzle Tool - 图片拼图工具 | By cmdragon
- 字幕下载工具 - 应用商店 | By cmdragon
- 歌词生成工具 - 应用商店 | By cmdragon
- 网盘资源聚合搜索 - 应用商店 | By cmdragon
- ASCII字符画生成器 - 应用商店 | By cmdragon
- JSON Web Tokens 工具 - 应用商店 | By cmdragon
- Bcrypt 密码工具 - 应用商店 | By cmdragon
- GIF 合成器 - 应用商店 | By cmdragon
- GIF 分解器 - 应用商店 | By cmdragon
- 文本隐写术 - 应用商店 | By cmdragon
- CMDragon 在线工具 - 高级AI工具箱与开发者套件 | 免费好用的在线工具
- 应用商店 - 发现1000+提升效率与开发的AI工具和实用程序 | 免费好用的在线工具
- CMDragon 更新日志 - 最新更新、功能与改进 | 免费好用的在线工具
- 支持我们 - 成为赞助者 | 免费好用的在线工具
- AI文本生成图像 - 应用商店 | 免费好用的在线工具
- 临时邮箱 - 应用商店 | 免费好用的在线工具
- 二维码解析器 - 应用商店 | 免费好用的在线工具
- 文本转思维导图 - 应用商店 | 免费好用的在线工具
- 正则表达式可视化工具 - 应用商店 | 免费好用的在线工具
- 文件隐写工具 - 应用商店 | 免费好用的在线工具
- IPTV 频道探索器 - 应用商店 | 免费好用的在线工具
- 快传 - 应用商店 | 免费好用的在线工具
- 随机抽奖工具 - 应用商店 | 免费好用的在线工具
- 动漫场景查找器 - 应用商店 | 免费好用的在线工具
- 时间工具箱 - 应用商店 | 免费好用的在线工具
- 网速测试 - 应用商店 | 免费好用的在线工具
- AI 智能抠图工具 - 应用商店 | 免费好用的在线工具
- 背景替换工具 - 应用商店 | 免费好用的在线工具
- 艺术二维码生成器 - 应用商店 | 免费好用的在线工具
- Open Graph 元标签生成器 - 应用商店 | 免费好用的在线工具
- 图像对比工具 - 应用商店 | 免费好用的在线工具
- 图片压缩专业版 - 应用商店 | 免费好用的在线工具
- 密码生成器 - 应用商店 | 免费好用的在线工具
- SVG优化器 - 应用商店 | 免费好用的在线工具
- 调色板生成器 - 应用商店 | 免费好用的在线工具
- 在线节拍器 - 应用商店 | 免费好用的在线工具
- IP归属地查询 - 应用商店 | 免费好用的在线工具
- CSS网格布局生成器 - 应用商店 | 免费好用的在线工具
- 邮箱验证工具 - 应用商店 | 免费好用的在线工具
- 书法练习字帖 - 应用商店 | 免费好用的在线工具
- 金融计算器套件 - 应用商店 | 免费好用的在线工具
- 中国亲戚关系计算器 - 应用商店 | 免费好用的在线工具
- Protocol Buffer 工具箱 - 应用商店 | 免费好用的在线工具
- IP归属地查询 - 应用商店 | 免费好用的在线工具
- 图片无损放大 - 应用商店 | 免费好用的在线工具
- 文本比较工具 - 应用商店 | 免费好用的在线工具
- IP批量查询工具 - 应用商店 | 免费好用的在线工具
- 域名查询工具 - 应用商店 | 免费好用的在线工具
- DNS工具箱 - 应用商店 | 免费好用的在线工具
- 网站图标生成器 - 应用商店 | 免费好用的在线工具
- XML Sitemap
4.2 一次性侦听器示例(选项式API)
export default {
watch: {
source: {
handler(newValue, oldValue) {
// 当source变化时,仅触发一次
},
once: true
}
}
}
4.3 一次性侦听器示例(组合式API)
watch(
source,
(newValue, oldValue) => {
// 当source变化时,仅触发一次
},
{ once: true }
)
五、watchEffect的使用
5.1 什么是watchEffect?
watchEffect允许我们自动跟踪回调的响应式依赖,它会立即执行,不需要指定immediate: true。在执行期间,它会自动追踪所有能访问到的响应式属性,每当这些属性变化时,回调会再次执行。
5.2 watchEffect示例
const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
5.3 注意事项
watchEffect仅会在其同步执行期间追踪依赖。在使用异步回调时,只有在第一个await正常工作前访问到的属性才会被追踪。
六、watch vs watchEffect
| 特性 | watch | watchEffect |
|---|---|---|
| 追踪方式 | 只追踪明确侦听的数据源 | 在副作用发生期间自动追踪所有能访问到的响应式属性 |
| 执行时机 | 懒执行,仅在数据源变化时触发 | 立即执行,之后在依赖变化时再次执行 |
| 回调参数 | 可以获取新旧值 | 无法直接获取新旧值 |
| 使用场景 | 需要精确控制触发时机,或者需要获取新旧值时 | 多个依赖项的侦听器,或者不需要关心具体变化值时 |
七、副作用清理
7.1 什么是副作用清理?
当我们在侦听器中执行异步操作时,可能会出现竞态问题。例如,在请求完成之前数据源发生了变化,当上一个请求完成时,它仍会使用已经过时的数据触发回调。这时我们需要清理这些过时的副作用。
7.2 副作用清理示例
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 回调逻辑
})
onCleanup(() => {
// 终止过期请求
controller.abort()
})
})
八、回调的触发时机
8.1 默认触发时机
默认情况下,侦听器回调会在父组件更新之后、所属组件的DOM更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的DOM,那么DOM将处于更新前的状态。
8.2 后置刷新的侦听器
如果想在侦听器回调中能访问被Vue更新之后的所属组件的DOM,你需要指明flush: 'post'选项:
watch(source, callback, {
flush: 'post'
})
// 或者使用watchPostEffect
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在Vue更新后执行 */
})
8.3 同步侦听器
你还可以创建一个同步触发的侦听器,它会在Vue进行任何更新之前触发:
watch(source, callback, {
flush: 'sync'
})
// 或者使用watchSyncEffect
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在响应式数据变化时同步执行 */
})
九、this.$watch的使用
9.1 什么是this.$watch?
我们也可以使用组件实例的$watch方法来命令式地创建一个侦听器。这在特定条件下设置侦听器,或者只侦听响应用户交互的内容时很有用。
9.2 this.$watch示例
export default {
created() {
this.$watch('question', (newQuestion) => {
// ...
})
}
}
十、停止侦听器
10.1 自动停止
用watch选项或者$watch()实例方法声明的侦听器,会在宿主组件卸载时自动停止。在setup()或script setup中用同步语句创建的侦听器,也会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。
10.2 手动停止
在少数情况下,你需要在组件卸载之前就停止一个侦听器,这时可以调用$watch() API返回的函数:
const unwatch = this.$watch('foo', callback)
// ...当该侦听器不再需要时
unwatch()
如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏:
<script setup>
import { watchEffect } from 'vue'
// 这个需要手动停止
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
</script>
十一、课后Quiz
问题1:如何在Vue3中侦听响应式对象的嵌套属性变化?
答案解析: 有两种方式可以侦听响应式对象的嵌套属性变化:
- 使用getter函数:
watch(
() => obj.nestedProperty,
(newValue) => {
console.log(newValue)
}
)
- 使用深层侦听器:
watch(obj, (newValue) => {
console.log(newValue.nestedProperty)
}, { deep: true })
注意:深层侦听器会遍历对象的所有嵌套属性,性能开销较大,建议优先使用getter函数的方式。
问题2:watch和watchEffect的主要区别是什么?
答案解析:
- watch是懒执行的,仅在数据源变化时触发回调,而watchEffect会立即执行,之后在依赖变化时再次执行。
- watch只追踪明确侦听的数据源,而watchEffect会自动追踪回调中访问到的所有响应式属性。
- watch可以获取新旧值,而watchEffect无法直接获取新旧值。
问题3:如何清理侦听器中的副作用?
答案解析: 可以通过onCleanup函数来清理副作用,它作为第三个参数传递给watch回调函数,或者作为第一个参数传递给watchEffect的作用函数:
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 回调逻辑
})
onCleanup(() => {
// 终止过期请求
controller.abort()
})
})
十二、常见报错解决方案
报错1:Cannot read property 'xxx' of undefined
产生原因: 在侦听器回调中访问了未初始化的响应式属性。
解决办法: 在访问属性之前先进行判断:
watchEffect(() => {
if (data.value) {
console.log(data.value.xxx)
}
})
报错2:Maximum call stack size exceeded
产生原因: 在侦听器回调中修改了被侦听的数据源,导致无限循环。
解决办法: 避免在侦听器回调中直接修改被侦听的数据源,或者使用条件判断来终止循环:
watch(count, (newCount) => {
if (newCount < 10) {
count.value++
}
})
报错3:Invalid watch source: xxx
产生原因: watch的第一个参数不是有效的数据源类型,比如直接传递了响应式对象的属性值。
解决办法: 使用getter函数来返回响应式对象的属性:
// 错误写法
watch(obj.count, (count) => {
console.log(count)
})
// 正确写法
watch(
() => obj.count,
(count) => {
console.log(count)
}
)