Vue 3 watch 与 watchEffect ,哪个更好?
Vue 3 watch 与 watchEffect ,哪个更好?
文章目录
- Vue 3 watch 与 watchEffect ,哪个更好?
- 1. 概述
- 2. watch 详解
- 2.1 基本语法
- 2.2 基本用法
- 2.3 侦听响应式对象
- 2.4 高级用法
- 3. watchEffect 详解
- 3.1 基本语法
- 3.2 基本用法
- 3.3 实际应用场景
- 4. 详细比较
- 4.1 语法和用法对比
- 4.2 代码对比示例
- 4.3 性能考虑示例
- 5. 高级功能和选项
- 5.1 刷新时机控制
- 5.2 调试功能
- 6. 实际应用场景
- 6.1 表单验证
- 6.2 API 数据获取
- 7. 最佳实践和性能优化
- 7.1 何时使用哪个
- 7.2 性能优化技巧
- 8. 总结
- watch 特点:
- watchEffect 特点:
- 选择建议:
1. 概述
Vue 3 提供了两种响应式侦听器:watch
和 watchEffect
。它们都用于响应数据变化执行副作用,但在使用方式和场景上有所不同。
2. watch 详解
2.1 基本语法
watch(source, callback, options?)
2.2 基本用法
<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 侦听响应式对象
<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: stringlastName: 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 高级用法
<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: numbername: 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 = falseonCleanup(() => {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 基本语法
watchEffect(effect, options?)
3.2 基本用法
<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.valueconsole.log(`计算: ${count.value} * ${multiplier.value} = ${result.value}`)
})// 副作用清理
watchEffect((onCleanup) => {const timer = setTimeout(() => {console.log('延迟执行的操作')}, 1000)onCleanup(() => {clearTimeout(timer)console.log('清理定时器')})
})
</script>
3.3 实际应用场景
<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 = truelet cancelled = falseonCleanup(() => {cancelled = trueloading.value = false})try {// 模拟图片加载const response = await fetch(url.value)if (cancelled) returnconst blob = await response.blob()if (cancelled) return// 创建对象 URLconst newBlobUrl = URL.createObjectURL(blob)// 清理之前的 URLif (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 代码对比示例
<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 性能考虑示例
<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: numbername: stringvalue: 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)) + 1items.value.push({id: newId,name: `项目${String.fromCharCode(64 + newId)}`,value: Math.random() * 100})
}
</script>
5. 高级功能和选项
5.1 刷新时机控制
<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 调试功能
<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 表单验证
<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 数据获取
<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: numbername: stringprice: numbercategory: 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 = trueerror.value = ''let cancelled = falseonCleanup(() => {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 性能优化技巧
<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
- 复杂场景可以组合使用