1. Vue3 响应式系统概述
Vue3 的响应式系统是整个框架的核心机制之一,它通过 Proxy 代理对象实现了数据的自动追踪和依赖收集。相比 Vue2 的 Object.defineProperty 实现,Vue3 的响应式系统在性能和功能上都有了显著提升。
在实际开发中,computed 和 watch 是响应式系统中最常用的两个 API。它们都能监听数据变化并执行相应操作,但设计理念和使用场景却大不相同。理解它们的内部原理和适用场景,对于编写高效、可维护的 Vue 应用至关重要。
2. computed 计算属性详解
2.1 computed 的基本用法
计算属性是基于它们的响应式依赖进行缓存的派生值。一个典型的 computed 使用示例如下:
import { ref, computed } from 'vue' const count = ref(0) const doubleCount = computed(() => count.value * 2)这里,doubleCount 会自动追踪 count 的变化,并在 count 变化时重新计算。但重要的是,如果 count 没有变化,多次访问 doubleCount 会直接返回缓存值而不会重新计算。
2.2 computed 的实现原理
Vue3 的 computed 内部实现主要依赖以下几个关键点:
- 惰性求值:computed 的值只有在被访问时才会计算
- 依赖追踪:通过 effect 函数建立响应式依赖关系
- 缓存机制:只有依赖发生变化时才会重新计算
- 触发更新:计算结果变化时触发组件重新渲染
在源码层面,computed 的实现可以简化为:
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.3 computed 的最佳实践
- 纯函数原则:计算属性应该是纯函数,不应该有副作用
- 避免复杂计算:计算逻辑应该尽量简单,复杂计算考虑使用 methods
- 合理使用 setter:需要设置计算属性时可以使用 setter 函数
- 性能优化:利用缓存特性减少不必要的计算
注意:在计算属性中执行异步操作或 DOM 操作是反模式,这种情况下应该使用 watch 或 watchEffect。
3. watch 侦听器深入解析
3.1 watch 的基本用法
watch API 用于观察特定数据源的变化,并在变化时执行回调函数。基本语法如下:
import { ref, watch } from 'vue' const count = ref(0) watch(count, (newVal, oldVal) => { console.log(`count changed from ${oldVal} to ${newVal}`) })3.2 watch 的高级用法
Vue3 的 watch API 提供了多种高级用法:
- 监听多个源:
watch([foo, bar], ([newFoo, newBar], [oldFoo, oldBar]) => { // 处理变化 })- 深度监听:
watch( () => state.someObject, (newVal, oldVal) => { // 在嵌套属性变化时触发 }, { deep: true } )- 立即执行:
watch( source, callback, { immediate: true } )- 回调执行时机:
watch( source, callback, { flush: 'post' } // 在组件更新后执行 )3.3 watch 的实现原理
watch 的实现基于 Vue3 的 effect 系统和调度器机制:
- 依赖收集:通过 effect 建立响应式依赖
- 变化检测:使用调度器控制回调执行时机
- 新旧值对比:保存旧值并在变化时提供新旧值对比
- 清理机制:支持在回调中返回清理函数
核心实现逻辑可以简化为:
function watch(source, cb, options = {}) { let getter if (isRef(source)) { getter = () => source.value } else if (isReactive(source)) { getter = () => source options.deep = true } else if (isFunction(source)) { getter = source } let oldValue const job = () => { const newValue = runner() cb(newValue, oldValue) oldValue = newValue } const runner = effect(getter, { lazy: true, scheduler: job }) if (options.immediate) { job() } else { oldValue = runner() } }4. computed 和 watch 的对比与选择
4.1 核心区别
| 特性 | computed | watch |
|---|---|---|
| 返回值 | 返回派生值 | 无返回值 |
| 缓存 | 有缓存 | 无缓存 |
| 异步支持 | 不支持 | 支持 |
| 执行时机 | 访问时计算 | 依赖变化时执行 |
| 使用场景 | 派生状态 | 副作用 |
4.2 何时使用 computed
- 需要从现有状态派生新状态时
- 需要缓存计算结果提高性能时
- 模板中需要复杂表达式简化时
- 需要响应式地计算一个值时
4.3 何时使用 watch
- 需要在状态变化时执行副作用时
- 需要执行异步操作时
- 需要监听深层嵌套对象变化时
- 需要在变化时访问旧值时
5. 常见问题与解决方案
5.1 computed 不更新的问题
问题现象:计算属性没有按预期更新
可能原因:
- 依赖项不是响应式的
- 在计算属性中使用了非响应式操作
- 依赖项变化但计算属性未被访问
解决方案:
- 确保所有依赖都是响应式数据(ref/reactive)
- 检查计算属性中是否有非响应式操作
- 使用 Vue Devtools 检查依赖关系
5.2 watch 多次触发问题
问题现象:watch 回调被多次执行
可能原因:
- 监听了整个响应式对象但没有使用 deep
- 多个依赖同时变化
- 在回调中修改了被监听的值
解决方案:
- 明确指定要监听的属性路径
- 使用 { flush: 'sync' } 控制执行时机
- 避免在回调中修改被监听的值
5.3 性能优化技巧
- 避免过度使用 deep watch:深度监听会遍历整个对象,性能开销大
- 合理使用 lazy watch:对于不立即需要的监听可以使用 { immediate: false }
- 使用 computed 替代复杂 watch:如果只是派生值,优先使用 computed
- 及时清理 watch:在组件卸载时清理不需要的 watch
6. 实战案例解析
6.1 表单验证场景
import { ref, computed } from 'vue' export function useFormValidation() { const username = ref('') const password = ref('') const isUsernameValid = computed(() => { return username.value.length >= 3 }) const isPasswordValid = computed(() => { return password.value.length >= 8 }) const isFormValid = computed(() => { return isUsernameValid.value && isPasswordValid.value }) return { username, password, isUsernameValid, isPasswordValid, isFormValid } }6.2 异步数据加载场景
import { ref, watch } from 'vue' import axios from 'axios' export function useUserData(userId) { const userData = ref(null) const loading = ref(false) const error = ref(null) watch( () => userId.value, async (newId) => { try { loading.value = true const response = await axios.get(`/api/users/${newId}`) userData.value = response.data } catch (err) { error.value = err } finally { loading.value = false } }, { immediate: true } ) return { userData, loading, error } }6.3 组合式API中的使用技巧
在组合式API中,可以结合生命周期钩子更好地管理 computed 和 watch:
import { onUnmounted, ref, computed, watch } from 'vue' export function useMousePosition() { const x = ref(0) const y = ref(0) const position = computed(() => ({ x: x.value, y: y.value })) const update = (e) => { x.value = e.pageX y.value = e.pageY } window.addEventListener('mousemove', update) const stopWatch = watch(position, (newPos) => { console.log('Position changed:', newPos) }) onUnmounted(() => { window.removeEventListener('mousemove', update) stopWatch() }) return { position } }7. 源码级优化技巧
7.1 自定义 computed 逻辑
在某些性能敏感场景,可以基于 effect 实现自定义 computed 逻辑:
import { effect, ref } from 'vue' export function useCustomComputed(getter, options = {}) { const value = ref() let dirty = true const runner = effect(getter, { lazy: true, scheduler: () => { if (!dirty) { dirty = true if (options.onDirty) { options.onDirty() } } } }) return { get value() { if (dirty) { value.value = runner() dirty = false } return value.value } } }7.2 高效 watch 模式
对于高频变化的数据源,可以使用防抖或节流优化 watch:
import { watch } from 'vue' import { throttle } from 'lodash-es' export function useThrottledWatch(source, cb, options = {}) { return watch( source, throttle(cb, options.wait || 100, { leading: options.leading !== false, trailing: options.trailing !== false }), options ) }7.3 响应式系统调试技巧
- 使用 effect 的 onTrack/onTrigger:
effect( () => { /* ... */ }, { onTrack(e) { debugger }, onTrigger(e) { debugger } } )- 自定义响应式调试工具:
function debugReactive(obj, name) { return new Proxy(obj, { get(target, key) { console.log(`[${name}] Get ${String(key)}`) return Reflect.get(target, key) }, set(target, key, value) { console.log(`[${name}] Set ${String(key)} to`, value) return Reflect.set(target, key, value) } }) }