下面,我们来系统的梳理关于 computed、watch 与 watchEffect 的基本知识点:
Vue 的响应式系统基于 依赖收集 和 触发更新 的机制:
特性 | computed | watch | watchEffect |
---|---|---|---|
返回值 | Ref 对象 | 停止函数 | 停止函数 |
依赖收集 | 自动 | 手动指定 | 自动 |
执行时机 | 惰性求值 | 响应变化 | 立即执行 |
主要用途 | 派生状态 | 响应变化执行操作 | 自动追踪副作用 |
新旧值 | 无 | 提供新旧值 | 无 |
异步支持 | 同步 | 支持异步 | 支持异步 |
首次执行 | 访问时执行 | 可配置 | 总是执行 |
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(' ')
}
})
.value
时计算// 避免在计算属性中产生副作用
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)
})
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 }
)
watch(source, callback, {
// 立即执行回调
immediate: true,
// 深度侦听
deep: true,
// 回调执行时机
flush: 'post', // 'pre' | 'post' | 'sync'
// 调试钩子
onTrack: (event) => debugger,
onTrigger: (event) => debugger
})
// 异步操作与取消
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)
)
// 避免深度侦听大型对象
watch(
() => state.items.length, // 仅侦听长度变化
(newLength) => {
console.log('Item count changed:', newLength)
}
)
// 使用浅层侦听
watch(
() => ({ ...shallowObject }), // 创建浅拷贝
(newObj) => {
console.log('Shallow object changed')
}
)
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()
onCleanup
回调// DOM 操作副作用
const elementRef = ref(null)
watchEffect(() => {
if (elementRef.value) {
// 操作 DOM
elementRef.value.focus()
}
})
// 响应式日志
watchEffect(() => {
console.log('State updated:', {
count: count.value,
user: user.value.name
})
})
// 组合多个副作用
watchEffect(async () => {
const data = await fetchData(params.value)
processData(data)
})
// 使用 flush 控制执行时机
watchEffect(
() => {
// 在 DOM 更新后执行
updateChart()
},
{ flush: 'post' }
)
// 调试依赖
watchEffect(
() => {
// 副作用代码
},
{
onTrack(e) {
debugger // 依赖被追踪时
},
onTrigger(e) {
debugger // 依赖变更触发回调时
}
}
)
场景 | 推荐 API | 理由 |
---|---|---|
派生状态 | computed | 自动缓存,高效计算 |
数据变化响应 | watch | 精确控制,获取新旧值 |
副作用管理 | watchEffect | 自动依赖收集 |
异步操作 | watch/watchEffect | 支持异步和取消 |
DOM 操作 | watchEffect | 自动追踪 DOM 依赖 |
调试依赖 | watch | 精确控制侦听源 |
// 计算属性 vs 侦听器
// 推荐:使用计算属性派生状态
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 不推荐:使用侦听器模拟计算属性
const fullName = ref('')
watch([firstName, lastName], () => {
fullName.value = `${firstName.value} ${lastName.value}`
})
// 组合 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) return
const controller = new AbortController()
onCleanup(() => controller.abort())
searchResults.value = await fetchSearchResults(
searchQuery.value,
controller.signal
)
})
class ComputedRefImpl {
constructor(getter, setter) {
this._getter = getter
this._setter = setter
this._value = undefined
this._dirty = true
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
trigger(this, 'value')
}
})
}
get value() {
if (this._dirty) {
this._value = this.effect.run()
this._dirty = false
track(this, 'value')
}
return this._value
}
set value(newValue) {
this._setter(newValue)
}
}
实现机制 | watch | watchEffect |
---|---|---|
依赖收集 | 基于指定源 | 自动收集执行中的依赖 |
内部实现 | 基于 watchEffect | 基础 API |
调度机制 | 支持 flush 配置 | 支持 flush 配置 |
清理机制 | 通过回调参数 | 通过 onCleanup |
避免不必要的重新计算:
// 使用 computed 缓存结果
const filteredList = computed(() =>
largeList.value.filter(item => item.active)
)
合理使用侦听选项:
// 减少深度侦听范围
watch(
() => state.user.id, // 仅侦听 ID 变化
(newId) => fetchUser(newId)
)
批量更新处理:
watch(
[data1, data2],
() => {
// 合并处理多个变化
updateVisualization()
},
{ flush: 'post' }
)
数据获取模式:
const data = ref(null)
const error = ref(null)
watchEffect(async (onCleanup) => {
data.value = null
error.value = null
const 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 }
)
问题: watchEffect 未正确追踪依赖
const state = reactive({ count: 0 })
watchEffect(() => {
// 当 state.count 变化时不会触发
console.log(state.nested?.value) // 可选链导致依赖丢失
})
解决方案:
watchEffect(() => {
// 显式访问确保依赖追踪
if (state.nested) {
console.log(state.nested.value)
}
})
问题: 多个异步请求可能导致旧数据覆盖新数据
watch(id, async (newId) => {
const data = await fetchData(newId)
currentData.value = data // 可能旧请求覆盖新
})
解决方案:
watch(id, async (newId, _, onCleanup) => {
let isCancelled = false
onCleanup(() => isCancelled = true)
const data = await fetchData(newId)
if (!isCancelled) {
currentData.value = data
}
})
问题: 侦听器中修改依赖数据导致循环
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
}
})