下面,我們來系統的梳理關于 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}
})