Vue中的computed與watch底層實現(xiàn)原理最佳實踐
更新時間:2026年01月21日 16:16:40 作者:LYFlied
本文介紹Vue中computed和watch的底層實現(xiàn)原理,對比了核心概念、設計差異以及使用場景,通過分析computed的初始化、依賴追蹤、緩存機制,以及watch的初始化、深度監(jiān)聽、異步更新,本文提供了一個全面的對比總結,并給出了性能優(yōu)化和最佳實踐建議,感興趣的朋友一起看看吧
Vue的computed與watch底層實現(xiàn)原理
1. 核心概念與設計差異
1.1 設計哲學對比
計算屬性 (computed)
- 目的:聲明式地描述一個派生值
- 特性:基于依賴緩存,惰性求值,響應式依賴追蹤
- 使用場景:模板中的復雜表達式、格式化數(shù)據(jù)、計算派生狀態(tài)
偵聽器 (watch)
- 目的:響應式地執(zhí)行副作用
- 特性:主動觀察,可執(zhí)行異步操作,獲取新舊值
- 使用場景:數(shù)據(jù)變化時執(zhí)行異步請求、執(zhí)行復雜邏輯、調試數(shù)據(jù)變化
1.2 核心差異總結
// 計算屬性 - 聲明式、緩存、同步
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
// 偵聽器 - 命令式、主動、可異步
watch: {
firstName(newVal, oldVal) {
// 執(zhí)行副作用
this.fetchUserData(newVal)
}
}2. computed底層實現(xiàn)原理
2.1 初始化過程
2.1.1 定義階段
// Vue內部處理computed選項
function initComputed(vm, computed) {
const watchers = (vm._computedWatchers = Object.create(null))
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 為每個計算屬性創(chuàng)建專門的Watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true } // 關鍵:標記為惰性求值
)
// 將計算屬性代理到Vue實例
defineComputed(vm, key, userDef)
}
}
// 定義計算屬性的getter/setter
function defineComputed(target, key, userDef) {
// 支持自定義setter
const setter = userDef.set || noop
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter,
enumerable: true,
configurable: true
})
}2.1.2 惰性求值實現(xiàn)
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 關鍵:只有當dirty為true時才重新計算
if (watcher.dirty) {
watcher.evaluate() // 執(zhí)行計算,并標記dirty為false
}
// 依賴收集:將當前渲染W(wǎng)atcher添加到計算屬性的依賴中
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}2.2 響應式依賴追蹤機制
2.2.1 依賴收集過程
// Watcher類的關鍵方法
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm
this.lazy = !!options.lazy // 是否為計算屬性Watcher
this.dirty = this.lazy // 惰性求值標記
// 計算屬性Watcher的getter是計算函數(shù)
this.getter = expOrFn
// 初始值
this.value = this.lazy ? undefined : this.get()
}
get() {
// 將當前Watcher推入棧頂
pushTarget(this)
let value
try {
// 執(zhí)行getter,觸發(fā)依賴收集
value = this.getter.call(this.vm, this.vm)
} finally {
// 恢復之前的Watcher
popTarget()
}
return value
}
evaluate() {
// 計算屬性的求值方法
this.value = this.get()
this.dirty = false // 標記為已計算
}
depend() {
// 讓依賴自己的Watcher收集自己作為依賴
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
update() {
// 計算屬性的更新策略
if (this.lazy) {
this.dirty = true // 僅標記為臟,不立即重新計算
} else {
// 普通Watcher的更新邏輯...
}
}
}2.2.2 依賴關系圖
模板渲染W(wǎng)atcher
↑
| depend()
計算屬性Watcher (lazy: true, dirty: false)
↑
| 在evaluate()中收集
依賴的響應式數(shù)據(jù) (firstName, lastName)2.3 緩存機制與性能優(yōu)化
2.3.1 緩存實現(xiàn)
// 計算屬性的緩存生命周期
const computedWatcher = new Watcher(vm, computedFn, null, { lazy: true })
// 第一次訪問:計算并緩存
computedWatcher.dirty = true → evaluate() → 計算結果 → dirty = false
// 第二次訪問(依賴未變):直接返回緩存
computedWatcher.dirty = false → 返回緩存值
// 依賴變化時:標記為臟
firstName改變 → computedWatcher.update() → dirty = true
// 再次訪問時重新計算
computedWatcher.dirty = true → evaluate() → 重新計算 → dirty = false2.3.2 多級計算屬性依賴
computed: {
// 計算屬性之間可以相互依賴
fullName() {
return this.firstName + ' ' + this.lastName
},
greeting() {
// 依賴另一個計算屬性
return 'Hello, ' + this.fullName
}
}
// 依賴鏈:
// greeting → fullName → (firstName, lastName)3. watch底層實現(xiàn)原理
3.1 初始化與Watcher創(chuàng)建
3.1.1 初始化過程
function initWatch(vm, watch) {
for (const key in watch) {
const handler = watch[key]
// 支持數(shù)組形式的多個handler
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher(vm, expOrFn, handler, options) {
// 處理對象形式的handler
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 支持字符串方法名
if (typeof handler === 'string') {
handler = vm[handler]
}
// 調用$watch API
return vm.$watch(expOrFn, handler, options)
}3.1.2 $watch API實現(xiàn)
Vue.prototype.$watch = function(expOrFn, cb, options) {
const vm = this
// 規(guī)范化參數(shù)
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true // 標記為用戶watcher
// 創(chuàng)建用戶Watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// 立即執(zhí)行選項
if (options.immediate) {
cb.call(vm, watcher.value)
}
// 返回取消監(jiān)聽函數(shù)
return function unwatchFn() {
watcher.teardown()
}
}3.2 深度監(jiān)聽實現(xiàn)
3.2.1 deep選項的實現(xiàn)
class Watcher {
constructor(vm, expOrFn, cb, options) {
// ...
this.deep = !!options.deep // 深度監(jiān)聽標記
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 解析表達式為getter函數(shù)
this.getter = parsePath(expOrFn)
}
this.value = this.get()
}
get() {
pushTarget(this)
let value
try {
value = this.getter.call(vm, vm)
} finally {
// 深度監(jiān)聽:遞歸遍歷對象
if (this.deep) {
traverse(value) // 遍歷對象所有屬性,觸發(fā)它們的getter
}
popTarget()
}
return value
}
}3.2.2 traverse遞歸遍歷
// 簡化的traverse實現(xiàn)
function traverse(val) {
const seenObjects = new Set()
function _traverse(val) {
// 跳過非對象、VNode和凍結對象
if (!val || typeof val !== 'object' || Object.isFrozen(val)) {
return
}
// 避免循環(huán)引用
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seenObjects.has(depId)) {
return
}
seenObjects.add(depId)
}
// 遞歸遍歷數(shù)組和對象
if (Array.isArray(val)) {
for (let i = 0; i < val.length; i++) {
_traverse(val[i])
}
} else {
const keys = Object.keys(val)
for (let i = 0; i < keys.length; i++) {
_traverse(val[keys[i]])
}
}
}
_traverse(val)
}3.3 異步更新與防抖控制
3.3.1 sync選項控制同步性
class Watcher {
update() {
if (this.sync) {
// 同步執(zhí)行
this.run()
} else if (this.lazy) {
// 計算屬性:僅標記為臟
this.dirty = true
} else {
// 默認:推入異步隊列
queueWatcher(this)
}
}
run() {
const value = this.get()
// 對于user watcher,調用回調并傳入新舊值
if (this.user) {
try {
this.cb.call(this.vm, value, this.value)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, this.value)
}
this.value = value
}
}3.3.2 支持異步回調
// watch示例:支持異步操作
watch: {
searchQuery: {
handler(newVal, oldVal) {
// 可以執(zhí)行異步操作
this.fetchResults(newVal)
},
immediate: true,
deep: false
}
}
// 底層實現(xiàn)支持異步回調
const watcher = new Watcher(vm, 'searchQuery', function(newVal, oldVal) {
// 回調函數(shù)可以是異步的
setTimeout(() => {
console.log('Async update:', newVal)
}, 0)
}, { user: true })4. 核心差異的詳細對比
4.1 設計模式差異
計算屬性(聲明式/響應式)
// 底層:依賴追蹤 + 惰性求值 + 緩存
// 類似:React的useMemo + 自動依賴追蹤
computed: {
// 聲明"什么值",由Vue負責"如何計算"
discountedPrice() {
return this.price * (1 - this.discount)
}
}偵聽器(命令式/副作用)
// 底層:觀察者模式 + 主動執(zhí)行
// 類似:React的useEffect
watch: {
// 聲明"當什么變化時,執(zhí)行什么操作"
price(newPrice) {
// 執(zhí)行副作用
this.logPriceChange(newPrice)
this.updateChart(newPrice)
}
}
4.2 執(zhí)行時機差異
4.2.1 計算屬性執(zhí)行時機
// 場景1:模板中使用計算屬性
<template>
<div>{{ discountedPrice }}</div>
</template>
// 執(zhí)行流程:
// 1. 模板編譯時發(fā)現(xiàn)discountedPrice
// 2. 觸發(fā)discountedPrice的getter
// 3. 如果dirty=true,執(zhí)行計算函數(shù)
// 4. 收集依賴(price, discount)
// 5. 緩存計算結果4.2.2 偵聽器執(zhí)行時機
// 場景:監(jiān)聽price變化
watch: {
price(newVal, oldVal) {
// 執(zhí)行流程:
// 1. price被修改,觸發(fā)setter
// 2. price的dep通知所有watcher
// 3. 找到對應的user watcher
// 4. 推入異步更新隊列
// 5. 下一個tick執(zhí)行回調
console.log(`Price changed from ${oldVal} to ${newVal}`)
}
}4.3 依賴處理差異
4.3.1 計算屬性的自動依賴收集
computed: {
userInfo() {
// Vue自動追蹤以下依賴:
// this.user.name, this.user.age, this.isVIP
return {
name: this.user.name,
age: this.user.age,
level: this.isVIP ? 'VIP' : 'Normal'
}
}
}
// 依賴關系自動建立,無需手動聲明
4.3.2 偵聽器的顯式依賴聲明
watch: {
// 需要明確指定要監(jiān)聽的數(shù)據(jù)路徑
'user.name': function(newName) {
// 只監(jiān)聽user.name的變化
},
// 或者監(jiān)聽整個對象(需要deep選項)
user: {
handler(newUser) {
// 監(jiān)聽user對象的任何屬性變化
},
deep: true
}
}5. 特殊場景與高級用法
5.1 計算屬性的setter
computed: {
fullName: {
// getter:計算邏輯
get() {
return this.firstName + ' ' + this.lastName
},
// setter:反向更新
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// 底層實現(xiàn)支持setter
function defineComputed(target, key, userDef) {
const getter = typeof userDef === 'function' ? userDef : userDef.get
const setter = userDef.set || noop // 支持自定義setter
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter
})
}5.2 偵聽器的立即執(zhí)行與防抖
watch: {
searchQuery: {
handler: 'fetchResults',
immediate: true, // 立即執(zhí)行一次
deep: false
}
}
// 結合lodash防抖
import { debounce } from 'lodash'
export default {
data() {
return { searchQuery: '' }
},
created() {
// 手動創(chuàng)建防抖的watcher
this.debouncedFetch = debounce(this.fetchResults, 500)
this.$watch('searchQuery', this.debouncedFetch)
},
methods: {
fetchResults(query) {
// API調用
}
}
}5.3 偵聽器返回取消函數(shù)
export default {
data() {
return { pollingInterval: null }
},
mounted() {
// $watch返回取消監(jiān)聽函數(shù)
const unwatch = this.$watch('dataId', (newId) => {
// 開始輪詢
this.startPolling(newId)
})
// 在組件銷毀時取消監(jiān)聽
this.$once('hook:beforeDestroy', () => {
unwatch()
this.stopPolling()
})
}
}6. 性能優(yōu)化與最佳實踐
6.1 計算屬性的性能優(yōu)勢
6.1.1 緩存避免重復計算
// 低效:每次渲染都重新計算
template: `<div>{{ expensiveComputation() }}</div>`
methods: {
expensiveComputation() {
// 每次渲染都會執(zhí)行
return heavyCalculation(this.data)
}
}
// 高效:使用計算屬性緩存
computed: {
computedResult() {
// 只在依賴變化時重新計算
return heavyCalculation(this.data)
}
}6.1.2 避免不必要的響應式
// 不推薦:在data中定義計算值
data() {
return {
// 這會使fullName成為響應式數(shù)據(jù),但實際應由其他數(shù)據(jù)計算得出
fullName: this.firstName + ' ' + this.lastName
}
}
// 推薦:使用計算屬性
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}6.2 偵聽器的性能注意事項
6.2.1 避免深度監(jiān)聽的性能開銷
// 謹慎使用:深度監(jiān)聽大型對象
watch: {
largeObject: {
handler() {
// 處理變化
},
deep: true // 遍歷整個對象,性能開銷大
}
}
// 優(yōu)化:監(jiān)聽具體屬性或使用計算屬性
watch: {
'largeObject.importantProp': function(newVal) {
// 只監(jiān)聽關鍵屬性
}
}6.2.2 及時清理偵聽器
export default {
data() {
return { externalData: null }
},
created() {
// 創(chuàng)建多個偵聽器
this.unwatch1 = this.$watch('filter', this.onFilterChange)
this.unwatch2 = this.$watch('sortBy', this.onSortChange)
},
beforeDestroy() {
// 清理偵聽器,避免內存泄漏
this.unwatch1()
this.unwatch2()
}
}7. Vue 3中的改進
7.1 Composition API中的實現(xiàn)
import { ref, computed, watch, watchEffect } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
// 計算屬性(響應式引用)
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})
// 偵聽器(支持多個源)
watch(
[firstName, lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('Name changed:', newFirst, newLast)
}
)
// 自動依賴追蹤的watchEffect
watchEffect(() => {
// 自動追蹤內部使用的響應式數(shù)據(jù)
document.title = `${firstName.value} ${lastName.value}`
})
return { firstName, lastName, fullName }
}
}7.2 Vue 3的底層優(yōu)化
// Vue 3使用Proxy實現(xiàn)響應式
const reactiveData = reactive({
firstName: 'John',
lastName: 'Doe'
})
// 計算屬性的實現(xiàn)更高效
const fullName = computed(() => {
return reactiveData.firstName + ' ' + reactiveData.lastName
})
// 偵聽器支持更多選項
watch(
() => reactiveData.firstName,
(newVal, oldVal) => {
console.log('First name changed')
},
{
immediate: true,
flush: 'post' // 在組件更新后執(zhí)行
}
)8. 總結
8.1 核心區(qū)別總結表
| 特性 | computed | watch |
|---|---|---|
| 設計目的 | 聲明派生狀態(tài) | 執(zhí)行副作用 |
| 緩存 | 有緩存,依賴不變不重新計算 | 無緩存,每次變化都執(zhí)行 |
| 執(zhí)行時機 | 惰性求值,訪問時計算 | 主動執(zhí)行,變化時觸發(fā) |
| 返回值 | 必須返回值 | 不需要返回值 |
| 異步支持 | 必須是同步計算 | 支持異步操作 |
| 依賴聲明 | 自動收集依賴 | 需要顯式聲明監(jiān)聽目標 |
| 使用場景 | 模板中的計算、數(shù)據(jù)格式化 | 異步操作、復雜邏輯、調試 |
8.2 選擇指南
使用computed當:
- 需要基于其他數(shù)據(jù)計算新值
- 需要在模板中使用的派生數(shù)據(jù)
- 需要緩存優(yōu)化性能
- 計算邏輯是純函數(shù),無副作用
使用watch當:
- 需要在數(shù)據(jù)變化時執(zhí)行異步操作
- 需要執(zhí)行副作用(API調用、DOM操作等)
- 需要知道數(shù)據(jù)變化前后的值
- 需要響應非響應式數(shù)據(jù)的變化
8.3 最佳實踐建議
- 優(yōu)先使用computed:對于派生數(shù)據(jù),計算屬性通常是更好的選擇
- 避免在computed中執(zhí)行副作用:保持計算屬性的純函數(shù)特性
- 謹慎使用deep watch:深度監(jiān)聽有性能開銷,盡量監(jiān)聽具體路徑
- 及時清理watch:在組件銷毀時取消不需要的偵聽器
- 考慮使用watchEffect:在Vue 3中,對于自動依賴追蹤的場景更簡潔
理解computed和watch的底層實現(xiàn)原理,可以幫助開發(fā)者更好地使用Vue的響應式系統(tǒng),編寫出更高效、更可維護的代碼。
到此這篇關于Vue中的computed與watch底層實現(xiàn)原理最佳實踐的文章就介紹到這了,更多相關vue computed與watch底層原理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue使用動態(tài)組件手寫Router View實現(xiàn)示例
這篇文章主要為大家介紹了vue使用動態(tài)組件手寫RouterView實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06
為什么Vue3.0使用Proxy實現(xiàn)數(shù)據(jù)監(jiān)聽(defineProperty表示不背這個鍋)
這篇文章主要介紹了為什么Vue3.0使用Proxy實現(xiàn)數(shù)據(jù)監(jiān)聽?defineProperty表示不背這個鍋,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10

