1. 为什么需要 computed 和 watch
在 Vue3 的响应式系统中,computed 和 watch 是两个核心概念。它们虽然都能响应数据变化,但设计初衷和使用场景完全不同。computed 用于派生状态的计算,而 watch 则用于观察数据变化并执行副作用操作。
1.1 computed 的本质
computed 属性本质上是一个"惰性求值"的计算属性。它会在依赖的响应式数据变化时重新计算,但只有在真正被访问时才会执行计算逻辑。这种设计带来了几个关键优势:
- 性能优化:避免不必要的重复计算
- 代码简洁:将复杂逻辑封装为声明式属性
- 缓存机制:相同依赖下直接返回缓存结果
javascript复制const count = ref(0)
const doubleCount = computed(() => count.value * 2)
1.2 watch 的适用场景
watch 更适合处理需要执行异步操作或复杂逻辑的副作用场景。比如:
- API 调用
- DOM 操作
- 本地存储操作
- 其他命令式操作
javascript复制watch(userId, async (newId) => {
const response = await fetchUser(newId)
userData.value = response.data
})
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. computed 的深度解析
2.1 computed 的实现原理
Vue3 的 computed 基于 Proxy 和 effect 实现。当创建一个 computed 时:
- 创建一个 effect 来跟踪依赖
- 设置 getter 触发依赖收集
- 设置缓存标记避免重复计算
javascript复制function computed(getter) {
let value
let dirty = true
const runner = effect(getter, {
lazy: true,
scheduler() {
dirty = true
trigger(obj, 'value')
}
})
const obj = {
get value() {
if (dirty) {
value = runner()
dirty = false
}
track(obj, 'value')
return value
}
}
return obj
}
2.2 computed 的常见问题
2.2.1 计算属性不更新
可能原因:
- 依赖项不是响应式的
- 在计算函数外部修改了依赖
- 使用了错误的响应式 API
解决方案:
javascript复制// 错误示例
const obj = { count: 0 }
const double = computed(() => obj.count * 2) // 不会更新
// 正确做法
const obj = reactive({ count: 0 })
const double = computed(() => obj.count * 2)
2.2.2 计算属性性能问题
当计算逻辑复杂时,可能会影响性能。优化方案:
- 拆分复杂计算为多个简单计算
- 使用 memoization 缓存结果
- 避免在计算属性中进行 DOM 操作
3. watch 的高级用法
3.1 watch 的多种形式
Vue3 提供了多种 watch 形式:
javascript复制// 基本形式
watch(source, callback)
// 监听多个源
watch([source1, source2], ([new1, new2], [old1, old2]) => {})
// 立即执行
watch(source, callback, { immediate: true })
// 深度监听
watch(source, callback, { deep: true })
// 回调刷新时机
watch(source, callback, { flush: 'post' })
3.2 watchEffect 的智能依赖收集
watchEffect 会自动收集依赖,简化代码:
javascript复制const count = ref(0)
const stop = watchEffect(() => {
console.log(count.value)
})
// 停止监听
stop()
注意点:
- 首次会立即执行
- 依赖是动态收集的
- 无法获取旧值
4. 实战中的选择策略
4.1 何时用 computed
适合使用 computed 的场景:
- 需要从现有数据派生新数据
- 需要缓存计算结果
- 需要响应式地组合多个值
- 逻辑是纯函数且无副作用
4.2 何时用 watch
适合使用 watch 的场景:
- 需要在数据变化时执行异步操作
- 需要执行有副作用的操作
- 需要监听深层嵌套对象的变化
- 需要精确控制回调执行时机
4.3 性能优化技巧
- 避免在 computed 中进行昂贵计算
- 合理使用 watch 的 flush 选项
- 对大型列表使用浅层监听
- 及时清理无用的 watch
javascript复制// 性能优化示例
const largeList = ref([...])
watch(
() => [...largeList.value], // 浅拷贝避免深层监听
() => { /* 处理逻辑 */ },
{ deep: false }
)
5. 常见问题解决方案
5.1 循环依赖问题
当 computed 相互依赖时可能导致无限循环:
javascript复制const a = computed(() => b.value + 1)
const b = computed(() => a.value + 1) // 循环依赖
解决方案:
- 重构逻辑消除循环
- 使用 watch 替代部分 computed
- 添加条件判断中断循环
5.2 异步 computed
Vue3 原生不支持异步 computed,但可以通过自定义实现:
javascript复制function asyncComputed(fn) {
const result = ref(null)
const loading = ref(false)
watchEffect(async () => {
loading.value = true
try {
result.value = await fn()
} finally {
loading.value = false
}
})
return { result, loading }
}
const { result, loading } = asyncComputed(async () => {
const res = await fetchData()
return res.data
})
5.3 watch 的内存泄漏
未清理的 watch 可能导致内存泄漏:
javascript复制// 错误示例 - 组件卸载后 watch 仍在运行
export default {
setup() {
watch(data, () => { /*...*/ })
}
}
// 正确做法 - 自动清理
export default {
setup() {
const stop = watch(data, () => { /*...*/ })
onUnmounted(stop)
}
}
6. 源码级原理剖析
6.1 computed 的响应式实现
Vue3 的 computed 核心实现位于 packages/reactivity/src/computed.ts。关键点:
- 通过
ComputedRefImpl类实现 - 使用
_dirty标记控制缓存 - 依赖收集通过
track和trigger完成
typescript复制class ComputedRefImpl<T> {
private _value!: T
private _dirty = true
constructor(
private getter: ComputedGetter<T>,
private _setter: ComputedSetter<T>
) {
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})
}
get value() {
if (this._dirty) {
this._value = this.effect()
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
6.2 watch 的底层机制
watch 的实现基于 doWatch 函数,核心流程:
- 标准化 source 为 getter 函数
- 创建 effect 并设置 scheduler
- 根据配置决定立即执行或延迟执行
typescript复制function doWatch(
source: WatchSource | WatchSource[],
cb: WatchCallback | null,
{ immediate, deep, flush }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
// 标准化 source
const getter = isArray(source)
? () => source.map(s => traverse(isRef(s) ? s.value : s))
: isRef(source)
? () => source.value
: isFunction(source)
? source
: () => traverse(source)
// 深度监听处理
if (deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
// 创建 scheduler
let scheduler: (job: () => any) => void
if (flush === 'sync') {
scheduler = job => job()
} else if (flush === 'post') {
scheduler = job => queuePostRenderEffect(job)
} else {
scheduler = job => queueJob(job)
}
// 创建 effect
const effect = new ReactiveEffect(getter, scheduler)
// 初始执行
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
}
// 返回停止函数
return () => effect.stop()
}
7. 最佳实践与性能模式
7.1 计算属性的优化模式
- 纯函数原则:保持计算属性无副作用
- 最小化依赖:只依赖必要的数据源
- 结果缓存:利用 computed 的缓存特性
- 延迟计算:复杂计算使用 defer 模式
javascript复制const expensiveCompute = computed(() => {
// 延迟计算示例
if (!needed.value) return null
return heavyCalculation(data.value)
})
7.2 watch 的高效使用
- 精确监听:避免不必要的 deep watch
- 防抖处理:高频变化使用 debounce
- 批量处理:合并多个 watch 为一个
- 及时清理:组件卸载时停止 watch
javascript复制// 防抖示例
import { debounce } from 'lodash-es'
watch(
searchQuery,
debounce((newVal) => {
fetchResults(newVal)
}, 500)
)
7.3 响应式数据设计模式
- 单一数据源:避免数据冗余
- 扁平化结构:减少深层嵌套
- 显式依赖:明确数据关系
- 合理拆分:大型状态分模块
javascript复制// 状态拆分示例
const useUser = () => {
const name = ref('')
const age = ref(0)
const fullName = computed(() => `${name.value} (${age.value})`)
return { name, age, fullName }
}
const { name, age, fullName } = useUser()
8. 与 Vue2 的对比分析
8.1 computed 的主要差异
-
实现机制:
- Vue2 基于 Object.defineProperty
- Vue3 基于 Proxy 和 effect
-
缓存行为:
- Vue2 总是缓存直到依赖变化
- Vue3 的缓存更智能,按需重新计算
-
类型支持:
- Vue3 有更好的 TypeScript 集成
8.2 watch 的主要改进
-
API 设计:
- Vue2 的 watch 是组件选项
- Vue3 的 watch 是组合式函数
-
性能优化:
- Vue3 的 watch 更轻量
- 支持更细粒度的控制
-
功能增强:
- 支持监听多个源
- 更好的异步处理
- 更灵活的调度控制
8.3 迁移策略
从 Vue2 迁移到 Vue3 时:
-
computed:
- 逻辑基本可以保持不变
- 注意响应式 API 的变化
-
watch:
- 将选项式 watch 改写为函数式
- 利用新的 watch API 简化代码
- 注意 immediate 和 deep 的默认值变化
javascript复制// Vue2 风格
export default {
watch: {
data: {
handler: 'fetchData',
immediate: true,
deep: true
}
}
}
// Vue3 风格
watch(
() => state.data,
fetchData,
{ immediate: true, deep: true }
)
9. 实际项目经验分享
9.1 电商平台中的实战案例
在商品筛选场景中:
javascript复制// 筛选条件
const filters = reactive({
category: '',
priceRange: [0, 1000],
ratings: 0
})
// 计算筛选结果
const filteredProducts = computed(() => {
return products.value.filter(p => {
return (
(!filters.category || p.category === filters.category) &&
p.price >= filters.priceRange[0] &&
p.price <= filters.priceRange[1] &&
p.rating >= filters.ratings
)
})
})
// 监听价格范围变化
watch(
() => filters.priceRange,
(newRange) => {
if (newRange[1] - newRange[0] < 100) {
showToast('价格区间太窄')
}
},
{ deep: true }
)
9.2 后台管理系统的应用
在表单验证场景:
javascript复制const form = reactive({
username: '',
email: '',
password: ''
})
// 计算表单是否有效
const isFormValid = computed(() => {
return (
form.username.length >= 3 &&
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email) &&
form.password.length >= 8
)
})
// 监听表单提交
watch(
() => form,
async (newForm) => {
if (isFormValid.value) {
const res = await submitForm(newForm)
if (res.success) {
redirectToDashboard()
}
}
},
{ deep: true }
)
9.3 移动端开发的特殊考量
在移动端性能敏感场景:
- 减少 computed 的复杂度
- 避免频繁触发的 watch
- 使用防抖和节流
- 注意内存泄漏问题
javascript复制// 移动端优化示例
const scrollPosition = ref(0)
const isScrollingDown = ref(false)
// 使用节流避免频繁更新
watch(
scrollPosition,
throttle((newPos, oldPos) => {
isScrollingDown.value = newPos > oldPos
}, 100)
)
10. 测试与调试技巧
10.1 computed 的单元测试
javascript复制import { computed, ref } from 'vue'
test('computed property', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
expect(double.value).toBe(0)
count.value = 5
expect(double.value).toBe(10)
})
10.2 watch 的行为验证
javascript复制import { watch, ref } from 'vue'
test('watch callback', async () => {
const count = ref(0)
let watchCount = 0
watch(count, () => {
watchCount++
})
count.value = 1
await nextTick()
expect(watchCount).toBe(1)
})
10.3 调试响应式依赖
使用 Vue Devtools 可以:
- 查看 computed 的依赖关系
- 检查 watch 的触发情况
- 分析响应式数据的变更历史
- 追踪组件更新的原因
javascript复制// 手动调试依赖
import { debug } from 'vue'
debug(doubledCount) // 在控制台输出依赖信息
11. 进阶模式与创新用法
11.1 动态 computed 属性
javascript复制const dynamicComputed = (getter) => {
const refMap = new Map()
return (key) => {
if (!refMap.has(key)) {
refMap.set(key, computed(() => getter(key)))
}
return refMap.get(key)
}
}
const getUserScore = dynamicComputed((userId) => {
return users.value.find(u => u.id === userId)?.score || 0
})
const score = getUserScore(123).value
11.2 链式 watch 模式
javascript复制function chainWatch(sources, callback) {
let stopHandlers = []
const start = () => {
stop()
for (let i = sources.length - 1; i >= 0; i--) {
const handler = watch(
sources[i],
(newVal, oldVal) => {
if (i === sources.length - 1) {
callback(newVal, oldVal, i)
}
}
)
stopHandlers.push(handler)
}
}
const stop = () => {
stopHandlers.forEach(h => h())
stopHandlers = []
}
start()
return stop
}
11.3 响应式状态机
javascript复制function useStateMachine(states, initialState) {
const current = ref(initialState)
const transition = (event) => {
const nextState = states[current.value]?.[event]
if (nextState) {
current.value = nextState
}
}
const is = (state) => computed(() => current.value === state)
return { current, transition, is }
}
const { current, transition, is } = useStateMachine({
idle: { START: 'loading' },
loading: { SUCCESS: 'success', ERROR: 'error' },
error: { RETRY: 'loading' }
}, 'idle')
watch(current, (newState) => {
console.log(`State changed to ${newState}`)
})
12. 生态集成与扩展
12.1 与 Pinia 的配合使用
在 Pinia store 中使用 computed 和 watch:
javascript复制export const useCartStore = defineStore('cart', () => {
const items = ref([])
const totalItems = computed(() => items.value.length)
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price, 0)
})
watch(totalPrice, (newTotal) => {
if (newTotal > 1000) {
showPremiumOffer()
}
})
return { items, totalItems, totalPrice }
})
12.2 与 Vue Router 的集成
监听路由变化并响应:
javascript复制import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(
() => route.params.id,
(newId) => {
fetchProduct(newId)
}
)
12.3 与第三方库的交互
与 axios 配合的典型模式:
javascript复制const data = ref(null)
const loading = ref(false)
const error = ref(null)
watch(
() => params.value,
async (newParams) => {
try {
loading.value = true
const response = await axios.get('/api/data', { params: newParams })
data.value = response.data
} catch (err) {
error.value = err
} finally {
loading.value = false
}
},
{ immediate: true }
)
13. 性能监控与优化
13.1 计算属性的性能测量
javascript复制import { computed, ref } from 'vue'
const measureComputed = (name, fn) => {
const start = performance.now()
const result = computed(fn)
const end = performance.now()
console.log(`Computed ${name} setup took ${end - start}ms`)
let runs = 0
let totalTime = 0
return new Proxy(result, {
get(target, prop) {
if (prop === 'value') {
const start = performance.now()
const value = target[prop]
const end = performance.now()
runs++
totalTime += end - start
if (runs % 10 === 0) {
console.log(`Computed ${name} average read time: ${totalTime / runs}ms`)
}
return value
}
return target[prop]
}
})
}
13.2 watch 的性能影响评估
javascript复制function instrumentedWatch(source, cb, options) {
let invocations = 0
let totalTime = 0
const instrumentedCb = (...args) => {
const start = performance.now()
cb(...args)
const end = performance.now()
invocations++
totalTime += end - start
if (invocations % 5 === 0) {
console.log(`Watch callback average time: ${totalTime / invocations}ms`)
}
}
return watch(source, instrumentedCb, options)
}
13.3 内存使用分析
使用 Chrome DevTools 的 Memory 面板:
- 记录堆快照
- 检查 ComputedRefImpl 实例数量
- 分析 ReactiveEffect 的内存占用
- 查找未清理的 watch
典型的内存泄漏模式:
javascript复制// 错误示例 - 闭包保留了组件实例
const useLeakyHook = () => {
const data = ref(null)
watch(someGlobalRef, (newVal) => {
data.value = process(newVal) // 保留了 data 引用
})
// 忘记返回 stop 函数
}
14. 未来演进与替代方案
14.1 Vue 3.3+ 的新特性
-
性能改进:
- 更高效的依赖追踪
- 减少不必要的重新计算
-
开发者体验:
- 更好的 TypeScript 支持
- 改进的 DevTools 集成
-
新功能:
- 实验性的 Suspense 集成
- 更灵活的响应式组合
14.2 Reactivity Transform 的影响
响应式语法糖提案带来的变化:
javascript复制// 传统方式
const count = ref(0)
const double = computed(() => count.value * 2)
// 使用 Reactivity Transform
let count = $ref(0)
const double = $computed(() => count * 2)
注意点:
- 语法糖最终会被编译为标准写法
- 不影响底层 computed/watch 机制
- 需要构建工具支持
14.3 与其他状态管理方案的对比
-
与 Redux 比较:
- Vue 的响应式更细粒度
- 不需要 action/reducer 样板代码
- 更适合组件局部状态
-
与 MobX 比较:
- 类似响应式理念
- Vue 的 API 更简洁
- 更好的框架集成
-
与 Signals 比较:
- 类似细粒度更新
- Vue 的 computed 更强大
- 更完整的生态系统
15. 总结与个人实践心得
在实际项目中使用 computed 和 watch 时,有几个关键体会:
-
明确区分目的:
- 需要派生数据时用 computed
- 需要执行副作用时用 watch
-
性能敏感场景:
- 避免在 computed 中进行 DOM 操作
- 高频变化的场景使用防抖/节流
-
代码组织:
- 将复杂逻辑拆分为多个简单 computed
- 相关的 watch 逻辑组织在一起
-
测试覆盖:
- 为关键 computed 属性编写测试
- 验证 watch 回调的触发条件
-
调试技巧:
- 使用 DevTools 检查依赖关系
- 添加日志辅助调试复杂场景
最后,理解底层响应式原理对于高效使用 computed 和 watch 至关重要。当遇到问题时,回归基本原理往往能找到最佳解决方案。
