computed()、watch() 与 watchEffect()
下面,我们来系统的梳理关于 computed、watch 与 watchEffect 的基本知识点:
一、核心概念与响应式基础
1.1 响应式依赖关系
Vue 的响应式系统基于 依赖收集 和 触发更新 的机制:
1.2 三大 API 对比
特性 | computed | watch | watchEffect |
---|---|---|---|
返回值 | Ref 对象 | 停止函数 | 停止函数 |
依赖收集 | 自动 | 手动指定 | 自动 |
执行时机 | 惰性求值 | 响应变化 | 立即执行 |
主要用途 | 派生状态 | 响应变化执行操作 | 自动追踪副作用 |
新旧值 | 无 | 提供新旧值 | 无 |
异步支持 | 同步 | 支持异步 | 支持异步 |
首次执行 | 访问时执行 | 可配置 | 总是执行 |
二、computed 深度解析
2.1 基本使用与类型
import { ref, computed } from 'vue'// 只读计算属性
const count = ref(0)
const double = computed(() => count.value * 2)// 可写计算属性
const fullName = computed({get: () => `${firstName.value} ${lastName.value}`,set: (newValue) => {[firstName.value, lastName.value] = newValue.split(' ')}
})
2.2 实现原理
2.3 核心特性
- 惰性求值:仅在访问
.value
时计算 - 结果缓存:依赖未变化时返回缓存值
- 依赖追踪:自动收集响应式依赖
- 类型安全:完美支持 TypeScript 类型推断
2.4 最佳实践
// 避免在计算属性中产生副作用
const badExample = computed(() => {console.log('This is a side effect!') // 避免return count.value * 2
})// 复杂计算使用计算属性
const totalPrice = computed(() => {return cartItems.value.reduce((total, item) => {return total + (item.price * item.quantity)}, 0)
})// 组合多个计算属性
const discountedTotal = computed(() => {return totalPrice.value * (1 - discountRate.value)
})
三、watch 深度解析
3.1 基本使用与语法
import { watch, ref } from 'vue'// 侦听单个源
const count = ref(0)
watch(count, (newValue, oldValue) => {console.log(`Count changed: ${oldValue} → ${newValue}`)
})// 侦听多个源
const firstName = ref('John')
const lastName = ref('Doe')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {console.log(`Name changed: ${oldFirst} ${oldLast} → ${newFirst} ${newLast}`)
})// 深度侦听对象
const state = reactive({ user: { name: 'Alice' } })
watch(() => state.user,(newUser, oldUser) => {console.log('User changed', newUser, oldUser)},{ deep: true }
)
3.2 配置选项详解
watch(source, callback, {// 立即执行回调immediate: true,// 深度侦听deep: true,// 回调执行时机flush: 'post', // 'pre' | 'post' | 'sync'// 调试钩子onTrack: (event) => debugger,onTrigger: (event) => debugger
})
3.3 高级用法
// 异步操作与取消
const data = ref(null)
watch(id, async (newId, oldId, onCleanup) => {const controller = new AbortController()onCleanup(() => controller.abort())try {const response = await fetch(`/api/data/${newId}`, {signal: controller.signal})data.value = await response.json()} catch (error) {if (error.name !== 'AbortError') {console.error('Fetch error:', error)}}
})// 限制执行频率
import { throttle } from 'lodash-es'
watch(searchQuery,throttle((newQuery) => {search(newQuery)}, 500)
)
3.4 性能优化
// 避免深度侦听大型对象
watch(() => state.items.length, // 仅侦听长度变化(newLength) => {console.log('Item count changed:', newLength)}
)// 使用浅层侦听
watch(() => ({ ...shallowObject }), // 创建浅拷贝(newObj) => {console.log('Shallow object changed')}
)
四、watchEffect 深度解析
4.1 基本使用
import { watchEffect, ref } from 'vue'const count = ref(0)// 自动追踪依赖
const stop = watchEffect((onCleanup) => {console.log(`Count: ${count.value}`)// 清理副作用onCleanup(() => {console.log('Cleanup previous effect')})
})// 停止侦听
stop()
4.2 核心特性
- 自动依赖收集:无需指定侦听源
- 立即执行:创建时立即运行一次
- 清理机制:提供
onCleanup
回调 - 异步支持:天然支持异步操作
4.3 高级用法
// DOM 操作副作用
const elementRef = ref(null)
watchEffect(() => {if (elementRef.value) {// 操作 DOMelementRef.value.focus()}
})// 响应式日志
watchEffect(() => {console.log('State updated:', {count: count.value,user: user.value.name})
})// 组合多个副作用
watchEffect(async () => {const data = await fetchData(params.value)processData(data)
})
4.4 性能优化
// 使用 flush 控制执行时机
watchEffect(() => {// 在 DOM 更新后执行updateChart()},{ flush: 'post' }
)// 调试依赖
watchEffect(() => {// 副作用代码},{onTrack(e) {debugger // 依赖被追踪时},onTrigger(e) {debugger // 依赖变更触发回调时}}
)
五、三者的区别与选择指南
5.1 使用场景对比
场景 | 推荐 API | 理由 |
---|---|---|
派生状态 | computed | 自动缓存,高效计算 |
数据变化响应 | watch | 精确控制,获取新旧值 |
副作用管理 | watchEffect | 自动依赖收集 |
异步操作 | watch/watchEffect | 支持异步和取消 |
DOM 操作 | watchEffect | 自动追踪 DOM 依赖 |
调试依赖 | watch | 精确控制侦听源 |
5.2 性能考虑
- computed:适合同步计算,避免复杂操作
- watch:适合需要精确控制的场景
- watchEffect:适合自动依赖收集的副作用
// 计算属性 vs 侦听器
// 推荐:使用计算属性派生状态
const fullName = computed(() => `${firstName.value} ${lastName.value}`)// 不推荐:使用侦听器模拟计算属性
const fullName = ref('')
watch([firstName, lastName], () => {fullName.value = `${firstName.value} ${lastName.value}`
})
5.3 组合使用模式
// 组合 computed 和 watch
const discountedTotal = computed(() => total.value * (1 - discount.value))watch(discountedTotal, (newTotal) => {updateUI(newTotal)
})// 组合 watchEffect 和 computed
const searchResults = ref([])
const searchQuery = ref('')const validQuery = computed(() => searchQuery.value.trim().length > 2
)watchEffect(async (onCleanup) => {if (!validQuery.value) returnconst controller = new AbortController()onCleanup(() => controller.abort())searchResults.value = await fetchSearchResults(searchQuery.value, controller.signal)
})
六、原理深入剖析
6.1 Vue 响应式系统核心
6.2 computed 实现原理
class ComputedRefImpl {constructor(getter, setter) {this._getter = getterthis._setter = setterthis._value = undefinedthis._dirty = truethis.effect = new ReactiveEffect(getter, () => {if (!this._dirty) {this._dirty = truetrigger(this, 'value')}})}get value() {if (this._dirty) {this._value = this.effect.run()this._dirty = falsetrack(this, 'value')}return this._value}set value(newValue) {this._setter(newValue)}
}
6.3 watch 和 watchEffect 的异同
实现机制 | watch | watchEffect |
---|---|---|
依赖收集 | 基于指定源 | 自动收集执行中的依赖 |
内部实现 | 基于 watchEffect | 基础 API |
调度机制 | 支持 flush 配置 | 支持 flush 配置 |
清理机制 | 通过回调参数 | 通过 onCleanup |
七、最佳实践与性能优化
7.1 性能优化策略
-
避免不必要的重新计算:
// 使用 computed 缓存结果 const filteredList = computed(() => largeList.value.filter(item => item.active) )
-
合理使用侦听选项:
// 减少深度侦听范围 watch(() => state.user.id, // 仅侦听 ID 变化(newId) => fetchUser(newId) )
-
批量更新处理:
watch([data1, data2],() => {// 合并处理多个变化updateVisualization()},{ flush: 'post' } )
7.2 常见模式
数据获取模式:
const data = ref(null)
const error = ref(null)watchEffect(async (onCleanup) => {data.value = nullerror.value = nullconst controller = new AbortController()onCleanup(() => controller.abort())try {const response = await fetch(url.value, {signal: controller.signal})data.value = await response.json()} catch (err) {if (err.name !== 'AbortError') {error.value = err.message}}
})
表单验证模式:
const formState = reactive({ email: '', password: '' })
const errors = reactive({ email: '', password: '' })watch(() => [formState.email, formState.password],() => {errors.email = formState.email.includes('@') ? '' : 'Invalid email'errors.password = formState.password.length >= 6 ? '' : 'Too short'},{ immediate: true }
)
八、常见问题与解决方案
8.1 响应式依赖问题
问题: watchEffect 未正确追踪依赖
const state = reactive({ count: 0 })watchEffect(() => {// 当 state.count 变化时不会触发console.log(state.nested?.value) // 可选链导致依赖丢失
})
解决方案:
watchEffect(() => {// 显式访问确保依赖追踪if (state.nested) {console.log(state.nested.value)}
})
8.2 异步竞态问题
问题: 多个异步请求可能导致旧数据覆盖新数据
watch(id, async (newId) => {const data = await fetchData(newId)currentData.value = data // 可能旧请求覆盖新
})
解决方案:
watch(id, async (newId, _, onCleanup) => {let isCancelled = falseonCleanup(() => isCancelled = true)const data = await fetchData(newId)if (!isCancelled) {currentData.value = data}
})
8.3 无限循环问题
问题: 侦听器中修改依赖数据导致循环
watch(count, (newVal) => {count.value = newVal + 1 // 无限循环
})
解决方案:
// 添加条件判断
watch(count, (newVal, oldVal) => {if (newVal < 100) {count.value = newVal + 1}
})// 使用 watchEffect 替代
watchEffect(() => {if (count.value < 100) {count.value += 1}
})