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

