VUE3:深入浅出watch用法大全
·
1. watch 对基本类型 vs 对象类型的区别
1.1 监听基本类型(ref)
<template>
<div>
<h2>监听基本类型 (ref)</h2>
<p>当前计数: {{ count }}</p>
<button @click="count++">+1</button>
<button @click="count--">-1</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// ✅ 直接监听 ref,不需要 .value
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
</script>
要点:
- 基本类型用
ref()包装 - watch 直接传入 ref 变量本身(不加
.value) - 当值改变时,watch 会自动触发
1.2 监听对象类型(reactive)
<template>
<div>
<h2>监听对象类型 (reactive)</h2>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="user.age++">年龄 +1</button>
<button @click="user.name = '小红'">改名为小红</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: '小明',
age: 18
})
// ⚠️ 监听整个 reactive 对象(默认就是深度监听)
watch(user, (newVal, oldVal) => {
// 注意:newVal 和 oldVal 是同一个对象引用
console.log('user 变化了', newVal)
console.log('newVal === oldVal:', newVal === oldVal) // true
})
</script>
重要区别:
|
特性 |
基本类型 (ref) |
对象类型 (reactive) |
|
监听方式 |
直接传入 ref |
直接传入 reactive 对象 |
|
默认深度监听 |
否 |
是(自动 deep) |
|
oldVal 准确性 |
✅ 准确 |
❌ 通常与 newVal 相同 |
1.3 监听对象的某个属性
<template>
<div>
<h2>监听对象的某个属性</h2>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="user.age++">年龄 +1</button>
<button @click="user.name = '小红'">改名为小红</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: '小明',
age: 18
})
// ✅ 监听对象的某个属性,需要用函数返回
watch(
() => user.age, // 👈 必须用箭头函数
(newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
}
)
// 也可以同时监听多个属性
watch(
() => user.name,
(newName, oldName) => {
console.log(`名字从 ${oldName} 变为 ${newName}`)
}
)
</script>
为什么要用函数?
user.age是一个普通值(如18),不是响应式的- 用
() => user.age让 Vue 每次重新获取最新值
1.4 监听 ref 包装的对象
<template>
<div>
<h2>监听 ref 包装的对象</h2>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="user.age++">年龄 +1</button>
<button @click="replaceUser">替换整个对象</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const user = ref({
name: '小明',
age: 18
})
// ❌ 这样不会监听到内部属性变化(只有整体替换才触发)
watch(user, (newVal) => {
console.log('user 整体被替换了', newVal)
})
// ✅ 方法1:添加 deep 选项(监听内部变化)
watch(user, (newVal) => {
console.log('user 内部变化了(deep)', newVal)
}, { deep: true })
// ✅ 方法2:监听具体属性
watch(
() => user.value.age,
(newAge, oldAge) => {
console.log(`age 从 ${oldAge} 变为 ${newAge}`)
}
)
// 替换整个对象
function replaceUser() {
user.value = { name: '小红', age: 20 }
}
</script>
2. 什么时候使用 deep
2.1 deep 的作用
<template>
<div>
<h2>deep 深度监听示例</h2>
<p>城市: {{ state.user.profile.address.city }}</p>
<button @click="changeCity">修改城市</button>
<button @click="replaceState">替换整个 state</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const state = ref({
user: {
profile: {
address: {
city: '北京'
}
}
}
})
// 没有 deep:只有 state.value 整体被替换才触发
watch(state, (newVal) => {
console.log('【无deep】state 变化了')
})
// 有 deep:任何嵌套属性变化都会触发
watch(state, (newVal) => {
console.log('【有deep】state 变化了,城市是:', newVal.user.profile.address.city)
}, { deep: true })
function changeCity() {
state.value.user.profile.address.city = '上海'
}
function replaceState() {
state.value = {
user: {
profile: {
address: {
city: '广州'
}
}
}
}
}
</script>
2.2 何时需要 deep
|
场景 |
是否需要 deep |
|
|
✅ 需要 |
|
|
❌ 不需要(默认就是深度监听) |
|
只关心对象的某个特定属性 |
❌ 不需要(用函数返回该属性) |
|
嵌套很深的对象 |
✅ 需要(但要注意性能) |
2.3 deep 的性能优化
<template>
<div>
<h2>deep 性能优化</h2>
<p>选中的项目: {{ selectedItem?.name }}</p>
<ul>
<li v-for="item in bigData.items" :key="item.id">
{{ item.name }}
<button @click="selectItem(item)">选择</button>
</li>
</ul>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const bigData = ref({
items: [
{ id: 1, name: '项目A', data: { /* 大量数据 */ } },
{ id: 2, name: '项目B', data: { /* 大量数据 */ } },
{ id: 3, name: '项目C', data: { /* 大量数据 */ } }
],
selectedId: null
})
const selectedItem = ref(null)
// 🚫 不推荐:深度监听大对象(性能差)
// watch(bigData, () => { ... }, { deep: true })
// ✅ 推荐:只监听你关心的属性
watch(
() => bigData.value.selectedId,
(newId) => {
console.log('选中的ID变化:', newId)
selectedItem.value = bigData.value.items.find(item => item.id === newId)
}
)
function selectItem(item) {
bigData.value.selectedId = item.id
}
</script>
3. watch 的其它属性
3.1 immediate - 立即执行
<template>
<div>
<h2>immediate 立即执行</h2>
<p>用户ID: {{ userId }}</p>
<p>用户信息: {{ userInfo }}</p>
<button @click="userId++">切换用户 (ID +1)</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const userId = ref(1)
const userInfo = ref('加载中...')
// 模拟获取用户信息
function fetchUser(id) {
return `用户${id}的信息`
}
// immediate: true 让回调立即执行一次
watch(userId, (newId) => {
console.log('获取用户信息,ID:', newId)
userInfo.value = fetchUser(newId)
}, { immediate: true })
// 页面加载时就会执行一次,不需要等 userId 变化
</script>
使用场景:
- 页面初始化时需要根据数据执行某些操作
- 需要立即获取初始值进行处理
3.2 flush - 执行时机
<template>
<div>
<h2>flush 执行时机</h2>
<p ref="countEl">计数: {{ count }}</p>
<button @click="count++">+1</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const countEl = ref(null)
// flush: 'pre' (默认) - DOM 更新前执行
watch(count, (newVal) => {
console.log('【pre】新值:', newVal)
console.log('【pre】DOM显示:', countEl.value?.textContent)
// DOM 还没更新,显示的是旧值
})
// flush: 'post' - DOM 更新后执行
watch(count, (newVal) => {
console.log('【post】新值:', newVal)
console.log('【post】DOM显示:', countEl.value?.textContent)
// DOM 已更新,显示的是新值
}, { flush: 'post' })
</script>
flush 选项说明:
'pre'(默认):DOM 更新前执行'post':DOM 更新后执行(适合需要访问更新后的 DOM)'sync':同步执行(谨慎使用,可能影响性能)
3.3 停止监听
<template>
<div>
<h2>停止监听</h2>
<p>计数: {{ count }}</p>
<p>监听状态: {{ isWatching ? '监听中' : '已停止' }}</p>
<button @click="count++">+1</button>
<button @click="toggleWatch">
{{ isWatching ? '停止监听' : '开始监听' }}
</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const isWatching = ref(true)
let stopWatch = null
// 创建监听器并保存停止函数
function createWatch() {
stopWatch = watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
isWatching.value = true
}
// 初始创建
createWatch()
function toggleWatch() {
if (isWatching.value) {
// 停止监听
stopWatch()
isWatching.value = false
console.log('已停止监听')
} else {
// 重新开始监听
createWatch()
console.log('已开始监听')
}
}
</script>
3.4 监听多个数据源
<template>
<div>
<h2>监听多个数据源</h2>
<p>姓名: {{ name }}</p>
<p>年龄: {{ age }}</p>
<p>完整信息: {{ fullInfo }}</p>
<button @click="name = '小红'">改名</button>
<button @click="age++">年龄+1</button>
<button @click="changeBoth">同时修改</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const name = ref('小明')
const age = ref(18)
const fullInfo = ref('')
// 监听多个数据源(数组形式)
watch(
[name, age], // 👈 数组形式
([newName, newAge], [oldName, oldAge]) => {
console.log(`名字:${oldName} → ${newName}`)
console.log(`年龄:${oldAge} → ${newAge}`)
fullInfo.value = `${newName}, ${newAge}岁`
},
{ immediate: true }
)
function changeBoth() {
name.value = '小刚'
age.value = 25
// 注意:虽然修改了两个值,但回调只会执行一次
}
</script>
3.5 once - 只执行一次(Vue 3.4+)
<template>
<div>
<h2>once 只执行一次</h2>
<p>计数: {{ count }}</p>
<p>首次变化记录: {{ firstChange }}</p>
<button @click="count++">+1</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const firstChange = ref('等待首次变化...')
// 只在第一次变化时执行,之后自动停止
watch(count, (newVal, oldVal) => {
console.log('这条日志只会出现一次')
firstChange.value = `首次变化:${oldVal} → ${newVal}`
}, { once: true })
</script>
4. computed vs watchEffect vs watch 的区别
4.1 computed - 计算属性
<template>
<div>
<h2>computed 计算属性</h2>
<p>单价: <input v-model.number="price" type="number" /></p>
<p>数量: <input v-model.number="quantity" type="number" /></p>
<p>总价: {{ total }}</p>
<p>折后价 (8折): {{ discountedTotal }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(2)
// computed 返回一个计算后的值
// 有缓存:只有依赖变化时才重新计算
const total = computed(() => {
console.log('计算 total') // 只有 price 或 quantity 变化才打印
return price.value * quantity.value
})
const discountedTotal = computed(() => {
console.log('计算 discountedTotal')
return total.value * 0.8
})
// 特点:
// ✅ 有缓存:依赖不变时不会重新计算
// ✅ 自动追踪依赖
// ✅ 返回一个 ref
// ❌ 不应该有副作用(不要在里面发请求、修改 DOM)
</script>
4.2 watch - 侦听器
<template>
<div>
<h2>watch 侦听器</h2>
<p>搜索关键词: <input v-model="keyword" /></p>
<p>搜索结果: {{ result }}</p>
<p>搜索状态: {{ status }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const result = ref('')
const status = ref('等待输入...')
// 模拟搜索API
function mockSearch(kw) {
return new Promise(resolve => {
setTimeout(() => {
resolve(`"${kw}" 的搜索结果`)
}, 500)
})
}
// watch:手动指定要监听的数据,执行副作用
watch(keyword, async (newKeyword, oldKeyword) => {
console.log(`关键词从 "${oldKeyword}" 变为 "${newKeyword}"`)
if (!newKeyword) {
result.value = ''
status.value = '等待输入...'
return
}
status.value = '搜索中...'
// 副作用:发起网络请求
result.value = await mockSearch(newKeyword)
status.value = '搜索完成'
})
// 特点:
// ✅ 可以获取新值和旧值
// ✅ 可以配置 immediate、deep 等选项
// ❌ 需要手动指定监听的数据源
</script>
4.3 watchEffect - 自动追踪的侦听器
<template>
<div>
<h2>watchEffect 自动追踪</h2>
<p>用户ID: <input v-model.number="userId" type="number" /></p>
<p>包含详情: <input v-model="includeDetails" type="checkbox" /></p>
<p>请求URL: {{ requestUrl }}</p>
<p>结果: {{ userData }}</p>
</div>
</template>
<script setup>
import { ref, watchEffect } from 'vue'
const userId = ref(1)
const includeDetails = ref(false)
const userData = ref('')
const requestUrl = ref('')
// 模拟API请求
function mockFetch(url) {
return new Promise(resolve => {
setTimeout(() => {
resolve(`请求 ${url} 的结果`)
}, 300)
})
}
// watchEffect:自动追踪函数内用到的所有响应式变量
watchEffect(async () => {
// Vue 自动知道这里依赖了 userId 和 includeDetails
const url = `/api/user/${userId.value}${includeDetails.value ? '?details=true' : ''}`
requestUrl.value = url
console.log('发起请求:', url)
userData.value = '加载中...'
userData.value = await mockFetch(url)
})
// 特点:
// ✅ 自动追踪依赖,不用手动指定
// ✅ 立即执行(相当于 immediate: true)
// ❌ 无法获取旧值
// ❌ 依赖不明确,可能追踪到不需要的变量
</script>
4.4 三者对比示例
<template>
<div>
<h2>computed vs watch vs watchEffect 对比</h2>
<p>firstName: <input v-model="firstName" /></p>
<p>lastName: <input v-model="lastName" /></p>
<hr />
<p><strong>computed 结果:</strong> {{ fullNameComputed }}</p>
<p><strong>watch 结果:</strong> {{ fullNameWatch }}</p>
<p><strong>watchEffect 结果:</strong> {{ fullNameWatchEffect }}</p>
<hr />
<p>查看控制台观察执行时机</p>
</div>
</template>
<script setup>
import { ref, computed, watch, watchEffect } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// ========== computed ==========
// 用于:计算派生数据
// 特点:有缓存、有返回值、自动追踪依赖
const fullNameComputed = computed(() => {
console.log('【computed】执行计算')
return firstName.value + lastName.value
})
// ========== watch ==========
// 用于:监听变化执行副作用
// 特点:手动指定依赖、可获取新旧值、无返回值
const fullNameWatch = ref('')
watch(
[firstName, lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('【watch】检测到变化')
console.log(` firstName: ${oldFirst} → ${newFirst}`)
console.log(` lastName: ${oldLast} → ${newLast}`)
fullNameWatch.value = newFirst + newLast
},
{ immediate: true }
)
// ========== watchEffect ==========
// 用于:自动追踪依赖并执行副作用
// 特点:自动追踪、立即执行、无法获取旧值
const fullNameWatchEffect = ref('')
watchEffect(() => {
console.log('【watchEffect】执行')
fullNameWatchEffect.value = firstName.value + lastName.value
})
</script>
4.5 如何选择?
需要计算派生数据?
└─ 是 → 用 computed
需要执行副作用(网络请求、DOM 操作等)?
└─ 是 → 需要旧值或精确控制?
└─ 是 → 用 watch
└─ 否 → 用 watchEffect
5. 什么是副作用
5.1 副作用的定义与示例
<template>
<div>
<h2>什么是副作用</h2>
<p>计数: {{ count }}</p>
<button @click="count++">+1</button>
<hr />
<p>纯函数结果: {{ pureResult }}</p>
<p>副作用计数器: {{ sideEffectCounter }}</p>
<p>页面标题已更新,查看浏览器标签</p>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
const count = ref(0)
const sideEffectCounter = ref(0)
// ========== 无副作用的函数(纯函数) ==========
// 只计算并返回,不影响外部
function pureAdd(a, b) {
return a + b
}
const pureResult = computed(() => {
return pureAdd(count.value, 10)
})
// ========== 有副作用的操作 ==========
watch(count, (newVal) => {
// 副作用1:修改外部变量
sideEffectCounter.value++
// 副作用2:修改 DOM(页面标题)
document.title = `计数: ${newVal}`
// 副作用3:控制台输出
console.log('count 变化了:', newVal)
// 副作用4:本地存储
localStorage.setItem('count', String(newVal))
})
</script>
5.2 常见的副作用类型
<template>
<div>
<h2>常见副作用类型演示</h2>
<p>搜索: <input v-model="keyword" /></p>
<button @click="keyword = ''">清空</button>
<hr />
<p>搜索结果: {{ searchResult }}</p>
<p>本地存储: {{ storedValue }}</p>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
const keyword = ref('')
const searchResult = ref('')
const storedValue = ref('')
// 读取本地存储
onMounted(() => {
storedValue.value = localStorage.getItem('lastKeyword') || '无'
})
watch(keyword, async (newKeyword) => {
if (!newKeyword) {
searchResult.value = ''
return
}
// ========== 副作用1:网络请求 ==========
console.log('副作用:发起网络请求')
// 模拟请求
await new Promise(resolve => setTimeout(resolve, 300))
searchResult.value = `"${newKeyword}" 的搜索结果`
// ========== 副作用2:DOM 操作 ==========
console.log('副作用:修改页面标题')
document.title = `搜索: ${newKeyword}`
// ========== 副作用3:本地存储 ==========
console.log('副作用:保存到 localStorage')
localStorage.setItem('lastKeyword', newKeyword)
storedValue.value = newKeyword
// ========== 副作用4:控制台输出 ==========
console.log('副作用:控制台输出 -', newKeyword)
})
</script>
5.3 为什么 computed 不应该有副作用
<template>
<div>
<h2>computed 不应有副作用</h2>
<p>单价: <input v-model.number="price" type="number" /></p>
<p>数量: <input v-model.number="quantity" type="number" /></p>
<hr />
<p>❌ 错误示范的总价: {{ badTotal }}</p>
<p>✅ 正确示范的总价: {{ goodTotal }}</p>
<p>请求计数: {{ requestCount }}</p>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const price = ref(100)
const quantity = ref(2)
const requestCount = ref(0)
// ❌ 错误示范:computed 中有副作用
const badTotal = computed(() => {
// 这些都是副作用,不应该在 computed 中
console.log('错误:computed 中打印日志')
requestCount.value++ // 错误:修改其他响应式数据
// fetch('/api/log') // 错误:发起网络请求
return price.value * quantity.value
})
// ✅ 正确示范:computed 只做纯计算
const goodTotal = computed(() => {
return price.value * quantity.value
})
// ✅ 副作用应该放在 watch 中
watch(goodTotal, (newTotal) => {
console.log('正确:在 watch 中打印日志')
console.log('总价变化:', newTotal)
// 这里可以发起请求、修改 DOM 等
})
</script>
为什么 computed 不能有副作用?
- computed 可能被多次调用,导致副作用重复执行
- 执行时机不确定,难以预测和调试
- 违反了 computed「纯计算」的设计初衷
总结
|
概念 |
核心要点 |
|
watch 基本类型 |
直接监听 ref,能拿到准确的新旧值 |
|
watch 对象类型 |
reactive 默认深度监听;ref 对象需要 |
|
deep |
深度监听嵌套属性,reactive 默认开启,ref 对象需手动开启 |
|
immediate |
立即执行一次回调 |
|
flush |
控制回调执行时机(DOM更新前/后) |
|
computed |
计算派生数据,有缓存,不应有副作用 |
|
watchEffect |
自动追踪依赖,立即执行,处理副作用 |
|
watch |
手动指定依赖,可配置选项,处理副作用 |
|
副作用 |
函数执行时对外部环境产生的影响(网络请求、DOM操作等) |
更多推荐


所有评论(0)