Vue 3 watch 与 watchEffect ,哪个更好?
文章目录
- [Vue 3 watch 与 watchEffect ,哪个更好?](#Vue 3 watch 与 watchEffect ,哪个更好?)
-
- [1. 概述](#1. 概述)
- [2. watch 详解](#2. watch 详解)
-
- [2.1 基本语法](#2.1 基本语法)
- [2.2 基本用法](#2.2 基本用法)
- [2.3 侦听响应式对象](#2.3 侦听响应式对象)
- [2.4 高级用法](#2.4 高级用法)
- [3. watchEffect 详解](#3. watchEffect 详解)
-
- [3.1 基本语法](#3.1 基本语法)
- [3.2 基本用法](#3.2 基本用法)
- [3.3 实际应用场景](#3.3 实际应用场景)
- [4. 详细比较](#4. 详细比较)
-
- [4.1 语法和用法对比](#4.1 语法和用法对比)
- [4.2 代码对比示例](#4.2 代码对比示例)
- [4.3 性能考虑示例](#4.3 性能考虑示例)
- [5. 高级功能和选项](#5. 高级功能和选项)
-
- [5.1 刷新时机控制](#5.1 刷新时机控制)
- [5.2 调试功能](#5.2 调试功能)
- [6. 实际应用场景](#6. 实际应用场景)
-
- [6.1 表单验证](#6.1 表单验证)
- [6.2 API 数据获取](#6.2 API 数据获取)
- [7. 最佳实践和性能优化](#7. 最佳实践和性能优化)
-
- [7.1 何时使用哪个](#7.1 何时使用哪个)
- [7.2 性能优化技巧](#7.2 性能优化技巧)
- [8. 总结](#8. 总结)
-
- [watch 特点:](#watch 特点:)
- [watchEffect 特点:](#watchEffect 特点:)
- 选择建议:
1. 概述
Vue 3 提供了两种响应式侦听器:watch 和 watchEffect。它们都用于响应数据变化执行副作用,但在使用方式和场景上有所不同。
2. watch 详解
2.1 基本语法
typescript
watch(source, callback, options?)
2.2 基本用法
vue
<template>
<div>
<input v-model="name" placeholder="姓名" />
<input v-model="age" type="number" placeholder="年龄" />
<p>姓名: {{ name }}, 年龄: {{ age }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const name = ref('')
const age = ref(0)
// 侦听单个 ref
watch(name, (newName, oldName) => {
console.log(`姓名从 "${oldName}" 变为 "${newName}"`)
})
// 侦听多个源
watch([name, age], ([newName, newAge], [oldName, oldAge]) => {
console.log(`姓名: ${oldName} -> ${newName}, 年龄: ${oldAge} -> ${newAge}`)
})
// 立即执行
watch(name, (newName, oldName) => {
console.log('立即执行,当前姓名:', newName)
}, { immediate: true })
// 深度侦听对象
const user = ref({
profile: {
firstName: '张',
lastName: '三'
}
})
watch(user, (newUser, oldUser) => {
console.log('用户信息变化:', newUser)
}, { deep: true })
</script>
2.3 侦听响应式对象
vue
<template>
<div>
<input v-model="user.firstName" placeholder="名" />
<input v-model="user.lastName" placeholder="姓" />
<p>全名: {{ fullName }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, watch, reactive, computed } from 'vue'
interface User {
firstName: string
lastName: string
}
const user = reactive<User>({
firstName: '',
lastName: ''
})
// 侦听 reactive 对象的特定属性
watch(
() => user.firstName,
(newFirstName, oldFirstName) => {
console.log(`名从 "${oldFirstName}" 变为 "${newFirstName}"`)
}
)
// 侦听多个属性
watch(
[() => user.firstName, () => user.lastName],
([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
console.log(`姓名变化: ${oldFirstName}${oldLastName} -> ${newFirstName}${newLastName}`)
}
)
const fullName = computed(() => `${user.firstName}${user.lastName}`)
</script>
2.4 高级用法
vue
<template>
<div>
<input v-model="searchQuery" placeholder="搜索..." />
<div v-if="loading">搜索中...</div>
<ul v-else>
<li v-for="result in searchResults" :key="result.id">
{{ result.name }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
interface SearchResult {
id: number
name: string
}
const searchQuery = ref('')
const searchResults = ref<SearchResult[]>([])
const loading = ref(false)
// 防抖搜索
watch(searchQuery, async (newQuery, oldQuery, onCleanup) => {
if (!newQuery.trim()) {
searchResults.value = []
return
}
loading.value = true
// 取消之前的请求
let cancelled = false
onCleanup(() => {
cancelled = true
})
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 500))
if (cancelled) return
// 模拟搜索结果
searchResults.value = [
{ id: 1, name: `${newQuery} 结果1` },
{ id: 2, name: `${newQuery} 结果2` },
{ id: 3, name: `${newQuery} 结果3` }
]
} catch (error) {
console.error('搜索失败:', error)
} finally {
if (!cancelled) {
loading.value = false
}
}
}, { immediate: true })
</script>
3. watchEffect 详解
3.1 基本语法
typescript
watchEffect(effect, options?)
3.2 基本用法
vue
<template>
<div>
<input v-model="count" type="number" />
<input v-model="multiplier" type="number" />
<p>结果: {{ result }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
const count = ref(1)
const multiplier = ref(2)
const result = ref(0)
// 自动追踪依赖
watchEffect(() => {
result.value = count.value * multiplier.value
console.log(`计算: ${count.value} * ${multiplier.value} = ${result.value}`)
})
// 副作用清理
watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log('延迟执行的操作')
}, 1000)
onCleanup(() => {
clearTimeout(timer)
console.log('清理定时器')
})
})
</script>
3.3 实际应用场景
vue
<template>
<div>
<input v-model="url" placeholder="图片URL" />
<div v-if="loading">加载中...</div>
<img v-else :src="blobUrl" alt="预览" style="max-width: 300px;" />
</div>
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
const url = ref('')
const blobUrl = ref('')
const loading = ref(false)
watchEffect(async (onCleanup) => {
if (!url.value) {
blobUrl.value = ''
return
}
loading.value = true
let cancelled = false
onCleanup(() => {
cancelled = true
loading.value = false
})
try {
// 模拟图片加载
const response = await fetch(url.value)
if (cancelled) return
const blob = await response.blob()
if (cancelled) return
// 创建对象 URL
const newBlobUrl = URL.createObjectURL(blob)
// 清理之前的 URL
if (blobUrl.value) {
URL.revokeObjectURL(blobUrl.value)
}
blobUrl.value = newBlobUrl
} catch (error) {
console.error('图片加载失败:', error)
blobUrl.value = ''
} finally {
if (!cancelled) {
loading.value = false
}
}
})
</script>
4. 详细比较
4.1 语法和用法对比
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖收集 | 显式声明侦听源 | 自动收集依赖 |
| 初始执行 | 需要 immediate: true |
立即执行 |
| 新旧值 | 提供新旧值 | 不提供新旧值 |
| 性能 | 精确控制侦听源 | 可能过度触发 |
| 使用场景 | 需要精确控制时 | 响应式副作用 |
4.2 代码对比示例
vue
<template>
<div>
<input v-model="firstName" placeholder="名" />
<input v-model="lastName" placeholder="姓" />
<p>使用 watch: {{ fullNameWatch }}</p>
<p>使用 watchEffect: {{ fullNameEffect }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const firstName = ref('')
const lastName = ref('')
const fullNameWatch = ref('')
const fullNameEffect = ref('')
// 使用 watch - 需要显式声明依赖
watch(
[firstName, lastName],
([newFirst, newLast]) => {
fullNameWatch.value = `${newFirst} ${newLast}`
console.log('watch - 全名更新:', fullNameWatch.value)
},
{ immediate: true }
)
// 使用 watchEffect - 自动收集依赖
watchEffect(() => {
fullNameEffect.value = `${firstName.value} ${lastName.value}`
console.log('watchEffect - 全名更新:', fullNameEffect.value)
})
</script>
4.3 性能考虑示例
vue
<template>
<div>
<input v-model="filter" placeholder="过滤条件" />
<input v-model="sortBy" placeholder="排序字段" />
<button @click="addItem">添加项目</button>
<ul>
<li v-for="item in displayedItems" :key="item.id">
{{ item.name }} - {{ item.value }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, watchEffect } from 'vue'
interface ListItem {
id: number
name: string
value: number
}
const items = ref<ListItem[]>([
{ id: 1, name: '项目A', value: 10 },
{ id: 2, name: '项目B', value: 20 },
{ id: 3, name: '项目C', value: 30 }
])
const filter = ref('')
const sortBy = ref<'name' | 'value'>('name')
// 使用 computed - 最佳性能
const displayedItems = computed(() => {
console.log('computed 执行')
let result = [...items.value]
if (filter.value) {
result = result.filter(item =>
item.name.toLowerCase().includes(filter.value.toLowerCase())
)
}
if (sortBy.value) {
result.sort((a, b) => a[sortBy.value].localeCompare?.(b[sortBy.value]) || a[sortBy.value] - b[sortBy.value])
}
return result
})
// 使用 watch - 精确控制
const filteredItems = ref<ListItem[]>([])
watch(
[() => items.value, filter, sortBy],
() => {
console.log('watch 执行')
let result = [...items.value]
if (filter.value) {
result = result.filter(item =>
item.name.toLowerCase().includes(filter.value.toLowerCase())
)
}
if (sortBy.value) {
result.sort((a, b) => a[sortBy.value].localeCompare?.(b[sortBy.value]) || a[sortBy.value] - b[sortBy.value])
}
filteredItems.value = result
},
{ immediate: true, deep: true }
)
// 使用 watchEffect - 可能过度触发
const effectItems = ref<ListItem[]>([])
watchEffect(() => {
console.log('watchEffect 执行')
let result = [...items.value]
if (filter.value) {
result = result.filter(item =>
item.name.toLowerCase().includes(filter.value.toLowerCase())
)
}
if (sortBy.value) {
result.sort((a, b) => a[sortBy.value].localeCompare?.(b[sortBy.value]) || a[sortBy.value] - b[sortBy.value])
}
effectItems.value = result
})
const addItem = () => {
const newId = Math.max(...items.value.map(i => i.id)) + 1
items.value.push({
id: newId,
name: `项目${String.fromCharCode(64 + newId)}`,
value: Math.random() * 100
})
}
</script>
5. 高级功能和选项
5.1 刷新时机控制
vue
<template>
<div>
<input v-model="text" placeholder="输入文本" />
<p>处理后的文本: {{ processedText }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, watch, watchEffect, nextTick } from 'vue'
const text = ref('')
const processedText = ref('')
// pre: DOM 更新前执行
watch(text, (newText) => {
processedText.value = newText.toUpperCase()
console.log('DOM 更新前执行')
}, { flush: 'pre' })
// post: DOM 更新后执行
watch(text, (newText) => {
console.log('DOM 更新后执行, 可以安全访问 DOM')
}, { flush: 'post' })
// sync: 同步执行(不推荐)
watch(text, (newText) => {
console.log('同步执行')
}, { flush: 'sync' })
// watchEffect 同样支持 flush 选项
watchEffect(() => {
console.log('当前文本:', text.value)
}, { flush: 'post' })
</script>
5.2 调试功能
vue
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const debugValue = ref('')
// 调试 watch
watch(debugValue, (newVal, oldVal) => {
console.log('值变化:', oldVal, '->', newVal)
}, {
onTrack: (e) => {
console.log('依赖被追踪', e)
},
onTrigger: (e) => {
console.log('依赖被触发', e)
}
})
// 调试 watchEffect
watchEffect(() => {
console.log('当前值:', debugValue.value)
}, {
onTrack: (e) => {
console.log('effect 依赖被追踪', e)
},
onTrigger: (e) => {
console.log('effect 依赖被触发', e)
}
})
</script>
6. 实际应用场景
6.1 表单验证
vue
<template>
<form @submit="handleSubmit">
<input v-model="email" placeholder="邮箱" />
<span v-if="emailError" class="error">{{ emailError }}</span>
<input v-model="password" type="password" placeholder="密码" />
<span v-if="passwordError" class="error">{{ passwordError }}</span>
<button type="submit" :disabled="!isFormValid">提交</button>
</form>
</template>
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const email = ref('')
const password = ref('')
const emailError = ref('')
const passwordError = ref('')
const isFormValid = ref(false)
// 使用 watch 进行表单验证
watch(email, (newEmail) => {
if (!newEmail) {
emailError.value = '邮箱不能为空'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
emailError.value = '邮箱格式不正确'
} else {
emailError.value = ''
}
}, { immediate: true })
watch(password, (newPassword) => {
if (!newPassword) {
passwordError.value = '密码不能为空'
} else if (newPassword.length < 6) {
passwordError.value = '密码至少6位'
} else {
passwordError.value = ''
}
}, { immediate: true })
// 使用 watchEffect 检查表单整体有效性
watchEffect(() => {
isFormValid.value = !emailError.value && !passwordError.value &&
email.value.length > 0 && password.value.length > 0
})
const handleSubmit = (e: Event) => {
e.preventDefault()
if (isFormValid.value) {
console.log('表单提交:', { email: email.value, password: password.value })
}
}
</script>
<style scoped>
.error {
color: red;
font-size: 12px;
}
</style>
6.2 API 数据获取
vue
<template>
<div>
<select v-model="selectedCategory">
<option value="">所有分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="books">图书</option>
</select>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<ul v-else>
<li v-for="product in products" :key="product.id">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
interface Product {
id: number
name: string
price: number
category: string
}
const selectedCategory = ref('')
const products = ref<Product[]>([])
const loading = ref(false)
const error = ref('')
// 使用 watch 处理 API 调用
watch(selectedCategory, async (newCategory, oldCategory, onCleanup) => {
loading.value = true
error.value = ''
let cancelled = false
onCleanup(() => {
cancelled = true
})
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000))
if (cancelled) return
// 模拟数据
const mockProducts: Product[] = [
{ id: 1, name: 'iPhone', price: 999, category: 'electronics' },
{ id: 2, name: 'T-shirt', price: 29, category: 'clothing' },
{ id: 3, name: 'Vue.js Guide', price: 39, category: 'books' },
{ id: 4, name: 'MacBook', price: 1999, category: 'electronics' },
{ id: 5, name: 'Jeans', price: 59, category: 'clothing' }
]
if (newCategory) {
products.value = mockProducts.filter(p => p.category === newCategory)
} else {
products.value = mockProducts
}
} catch (err) {
error.value = '加载失败'
console.error('API 错误:', err)
} finally {
if (!cancelled) {
loading.value = false
}
}
}, { immediate: true })
</script>
7. 最佳实践和性能优化
7.1 何时使用哪个
使用 watch 的情况:
- 需要访问旧值和新值
- 需要精确控制侦听的源
- 需要懒执行(非立即执行)
- 处理昂贵的操作需要防抖
使用 watchEffect 的情况:
- 简单的响应式副作用
- 依赖关系复杂或动态变化
- 需要立即执行
- 逻辑简单,不需要旧值
7.2 性能优化技巧
vue
<script setup lang="ts">
import { ref, watch, watchEffect, computed } from 'vue'
const expensiveData = ref({/* 大量数据 */})
const filterCondition = ref('')
// 不好的做法:深度侦听大量数据
watch(expensiveData, () => {
// 每次 expensiveData 的任何变化都会触发
}, { deep: true })
// 好的做法:精确侦听需要的部分
watch(
() => expensiveData.value.someSpecificProperty,
() => {
// 只有特定属性变化时触发
}
)
// 使用 computed 进行复杂计算
const filteredData = computed(() => {
return expensiveData.value.filter(item =>
item.name.includes(filterCondition.value)
)
})
// 只在必要时执行
const shouldWatch = ref(false)
watch(
() => shouldWatch.value ? expensiveData.value : null,
() => {
// 只有 shouldWatch 为 true 时执行
}
)
</script>
8. 总结
watch 特点:
- 精确控制:显式声明侦听源
- 访问旧值:可以比较变化前后的值
- 灵活配置:支持 immediate、deep 等选项
- 性能优化:避免不必要的执行
watchEffect 特点:
- 自动追踪:自动收集响应式依赖
- 立即执行:创建后立即运行一次
- 简洁语法:不需要显式声明依赖
- 动态依赖:依赖关系可以动态变化
选择建议:
- 大多数情况下,优先考虑使用
computed - 需要副作用且依赖明确时使用
watch - 简单的响应式副作用使用
watchEffect - 复杂场景可以组合使用