?一、計算屬性(computed
)
計算屬性(Computed Properties)是 Vue 中一種特殊的響應式數據,它能基于已有的響應式數據動態計算出新的數據。
計算屬性有以下特性:
-
自動緩存:只有當它依賴的響應式數據發生變化時,才會重新計算。
-
響應式更新:依賴的數據變化后,會自動觸發計算屬性重新計算。
-
簡化模板:在模板中使用計算屬性可以減少復雜邏輯,讓模板更清晰、易讀。
簡單來說:
計算屬性是基于其他響應式數據而自動計算得到的值,且具有緩存和響應式的特性。
1、計算屬性的基本用法
<script setup>
import { ref, computed } from 'vue'// 響應式數據
const firstName = ref('Tom')
const lastName = ref('Jerry')// 計算屬性(根據響應式數據動態計算)
const fullName = computed(() => {return `${firstName.value} ${lastName.value}`
})
</script><template><div>{{ fullName }}</div> <!-- 顯示:Tom Jerry -->
</template>
注意默認計算屬性是只讀的,但也可以定義成可寫。當你嘗試修改一個計算屬性時,你會收到一個運行時警告。只在某些特殊場景中你可能才需要用到“可寫”的屬性,你可以通過同時提供 getter 和 setter 來創建:
<script setup>
import { ref, computed } from 'vue'const firstName = ref('John')
const lastName = ref('Doe')const fullName = computed({// getterget() {return firstName.value + ' ' + lastName.value},// setterset(newValue) {// 注意:我們這里使用的是解構賦值語法[firstName.value, lastName.value] = newValue.split(' ')}
})
</script>
?現在當你再運行?fullName.value = 'John Doe'
?時,setter 會被調用而?firstName
?和?lastName
?會隨之更新。
注意,computed()里面不接受任何的參數,我們看到里面有一個回調函數,這個回調函數本質上是getter函數
-
之前版本(<3.4),這個 getter 函數沒有參數。
-
從 3.4 開始,這個 getter 函數可以接受一個參數:就是上一次計算屬性的計算結果。
簡單來說:
如果需要,可以通過訪問計算屬性的 getter 的第一個參數來獲取計算屬性返回的上一個值:
<script setup>
import { ref, computed } from 'vue'const count = ref(2)// 這個計算屬性在 count 的值小于或等于 3 時,將返回 count 的值。
// 當 count 的值大于等于 4 時,將會返回滿足我們條件的最后一個值
// 直到 count 的值再次小于或等于 3 為止。
const alwaysSmall = computed((previous) => {if (count.value <= 3) {return count.value}return previous
})
</script>
?如果你正在使用可寫的計算屬性的話:
<script setup>
import { ref, computed } from 'vue'const count = ref(2)const alwaysSmall = computed({get(previous) {if (count.value <= 3) {return count.value}return previous},set(newValue) {count.value = newValue * 2}
})
</script>
?2、計算屬性與方法(methods)的詳細區別
兩者區別如下:
對比維度 | 計算屬性(computed) | 方法(methods) |
---|---|---|
緩存機制 | 有緩存,僅數據變化才重新計算 | 無緩存,每次調用都會執行 |
調用方式 | 不需要括號調用,像屬性一樣使用 | 需要括號調用,明確為函數 |
適用場景 | 基于響應式數據的計算 | 處理事件或顯式調用的場景 |
性能開銷 | 性能較高(緩存優化) | 性能較低(頻繁調用時) |
計算屬性與方法性能差異分析
假設模板多次渲染對比:
-
計算屬性:
<div>{{ doubleCount }}</div> <div>{{ doubleCount }}</div> <div>{{ doubleCount }}</div>
-
只計算一次,緩存結果。
-
-
方法調用:
<div>{{ getDoubleCount() }}</div> <div>{{ getDoubleCount() }}</div> <div>{{ getDoubleCount() }}</div>
-
每次都調用一次,共調用3次,性能浪費。
-
因此,對于頻繁使用但數據不頻繁變化的場景,建議使用計算屬性。
3、計算屬性什么時候不能用?
計算屬性適用于:
-
同步、快速的計算邏輯。
-
無副作用的計算(純函數)。
計算屬性不適合:
-
異步邏輯(如請求數據)。
-
執行副作用(修改其他數據、DOM 操作)。
?二、監聽屬性
在 Vue 中,監聽屬性(Watch) 是一種響應式機制,用于監測響應式數據的變化:
-
當你想在數據發生變化時執行某些邏輯(如發送請求、更新數據或執行某些副作用)時,就可以使用監聽屬性。
-
監聽屬性通過 Vue 提供的
watch()
或watchEffect()
函數實現。
簡單來說:
監聽屬性讓你能夠對數據變化做出反應,執行一些副作用或異步操作。
1、監聽屬性的基本用法(watch)
<script setup>
import { ref, watch } from 'vue'const count = ref(0)// 監聽 count 的變化
watch(count, (newValue, oldValue) => {console.log(`count變化了:從${oldValue}到${newValue}`)
})
</script><template><button @click="count++">增加 ({{ count }})</button>
</template>
當 count
的值改變時,watch
會自動觸發,執行回調函數。
監聽多個數據:?
const firstName = ref('Tom')
const lastName = ref('Jerry')// 同時監聽 firstName 和 lastName
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {console.log(`名字變化了:${oldFirst} ${oldLast} => ${newFirst} ${newLast}`)
})
2、監聽屬性的參數與選項(高級用法)
🔹 監聽屬性的函數簽名:watch(source, callback, options)
-
source
: 需要監聽的響應式數據,可以是單個或多個。可以是不同形式的“數據源”:它可以是一個 ref (包括計算屬性)、一個響應式對象、一個?getter 函數、或多個數據源組成的數組: -
callback
: 數據變化時執行的回調函數。 -
options
: 可選參數,控制監聽器行為。
🔹 常用的監聽選項(options):
選項 | 含義 | 默認值 |
---|---|---|
immediate | 是否立即執行一次監聽回調 | false |
deep | 是否深度監聽對象內部屬性變化 | false |
flush | 控制監聽器回調的執行時機 | 'pre' |
?深度監聽(deep)到底是什么?
-
默認情況下,Vue 的監聽器只能監聽對象引用本身的變化(比如替換對象)。
-
使用
deep: true
時,能監聽對象或數組內部屬性或元素的變化。
const user = ref({ name: 'Tom', age: 18 })// 默認淺監聽(只能監聽整個對象變化)
watch(user, () => {console.log('淺監聽:user變化了')
})user.value.age = 19 // ?不會觸發淺監聽(對象引用未變)
user.value = { name: 'Jerry', age: 19 } // ??觸發// 深度監聽(對象內部屬性變化也會觸發)
watch(user, () => {console.log('深監聽:user變化了')
}, { deep: true })user.value.age = 20 // ??觸發深度監聽
?監聽屬性的執行時機(flush)
flush
控制監聽回調的執行時機:
flush 值 | 含義 | 使用場景 |
---|---|---|
pre | 默認值,組件更新之前執行 | 大多數情況 |
post | 組件更新之后執行 | 需要訪問更新后的DOM時 |
sync | 同步觸發,數據變化立即執行 | 非常特殊情況 |
3、watch vs watchEffect 的區別
在 Vue 中,watch()
和 watchEffect()
都用于響應式地執行一些副作用操作(如發起請求、改變 DOM),但二者的追蹤數據依賴方式不同:
特性 | watch() | watchEffect() |
---|---|---|
如何追蹤依賴 | 手動顯式指定要監聽的數據(明確) | 自動追蹤回調中訪問的數據(隱式) |
首次執行 | 默認不立即執行,需手動開啟 | 自動立即執行一次 |
控制粒度 | 精確控制監聽的數據項,控制更細 | 自動追蹤所有訪問的響應式數據,更靈活 |
適用場景 | 明確知道監聽什么數據變化 | 數據依賴較多或復雜,更希望自動追蹤 |
?舉個簡單例子,監聽單個明確的數據:
const todoId = ref(1)
const data = ref(null)watch(todoId,async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()},{ immediate: true }
)
?特點:
-
todoId
被顯式聲明為監聽的源。 -
回調函數只在明確的源數據(
todoId
)改變時觸發。 -
必須用
{ immediate: true }
來立即執行一次,否則首次不會執行。
1、?watchEffect()
如何簡化上面的例子?
const todoId = ref(1)
const data = ref(null)watchEffect(async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()
})
特點:
-
自動立即執行一次,無需手動指定
{ immediate: true }
。 -
無需手動指定監聽源,回調函數內的所有響應式數據訪問(這里是
todoId.value
)會自動被 Vue 跟蹤。 -
一旦被跟蹤的數據變化(例如:
todoId.value
改變),回調會自動再次執行。
?2、watchEffect()
的依賴跟蹤原理(關鍵):
watchEffect()
會自動追蹤回調函數在同步執行時訪問的所有響應式數據。
但有個重要提示:
如果回調是異步函數,那么只有在第一個
await
之前訪問的數據才會被追蹤!
watchEffect(async () => {console.log(todoId.value) // ? 被追蹤,因為在 await 之前訪問const response = await fetch('...')console.log(someOtherRef.value) // ? 不被追蹤,因為在 await 之后訪問
})
原因是:
-
Vue 只能追蹤同步執行階段訪問的數據。
-
在異步操作完成后的回調內訪問的數據不會被 Vue 追蹤。
3、watchEffect()
在實際場景中的優勢:
優勢一:自動跟蹤多個數據源(不必手動指定):
假如你有多個響應式數據:
const firstName = ref('Tom')
const lastName = ref('Jerry')watchEffect(() => {console.log(`Name: ${firstName.value} ${lastName.value}`)
})Vue 自動監控 firstName 和 lastName。無論哪個改變,都會觸發回調函數。使用 watch() 則必須手動指定數據源:
watch([firstName, lastName], () => {console.log(`Name: ${firstName.value} ${lastName.value}`)
})
優勢二:更精細地跟蹤對象屬性(比深監聽高效):
假如你有復雜對象:
const user = ref({name: 'Tom',age: 20,address: { city: 'Shanghai', street: 'Main St' }
})watchEffect(() => {console.log(`User city: ${user.value.address.city}`)
})watchEffect() 只監聽了對象的部分屬性 (address.city),高效、精準。如果用深監聽 (watch(user, ..., { deep: true })),會監聽所有屬性的變化,性能可能較差。
4、什么時候用 watch()
?什么時候用 watchEffect()
?
場景 | 推薦方式 | 理由 |
---|---|---|
明確知道監聽的數據源 | ? 使用 watch() | 明確指定,粒度精準 |
多個數據源或依賴復雜 | ? 使用 watchEffect() | 自動跟蹤,代碼更簡潔、更靈活 |
動態數據請求或復雜副作用 | ? watchEffect() | 自動監聽,省去手動指定煩惱 |
4、監聽屬性的常見使用場景
場景 | 示例 |
---|---|
數據變化請求API | 表單值變化時重新獲取數據 |
數據變化存儲數據 | 自動保存用戶輸入 |
執行副作用 | 數據變化時更新DOM或執行動畫 |
5、監聽屬性的注意事項?
-
避免無限循環:
watch(count, (val) => {count.value++ // ?? 無限循環,不要這樣做 })
-
不要監聽非響應式數據(監聽無效):
const plain = { name: 'Tom' } watch(plain, () => {}) // ? 無效
-
使用深監聽時注意性能問題(深監聽成本較高):
watch(obj, () => {}, { deep: true }) // 謹慎使用
注意,你不能直接偵聽響應式對象的屬性值,例如、
const obj = reactive({ count: 0 })// 錯誤,因為 watch() 得到的參數是一個 number
watch(obj.count, (count) => {console.log(`Count is: ${count}`)
})
?這里需要用一個返回該屬性的 getter 函數:
// 提供一個 getter 函數
watch(() => obj.count,(count) => {console.log(`Count is: ${count}`)}
)
6、副作用清理
在 Vue 中,所謂的副作用通常指:
-
異步請求(如 API 請求)
-
定時器 (
setTimeout
、setInterval
) -
DOM 操作、監聽事件
-
其他非純函數的邏輯
這些操作不是立即完成的,可能會在未來某個時刻繼續執行。
?為什么需要副作用清理?
以 API 請求為例:假設我們有一個監聽器監聽 id:
watch(id, (newId) => {fetch(`/api/${newId}`).then(() => {console.log('請求完成,當前ID:', newId)})
})
可能的問題:
當你快速修改 id
:
-
請求 1 (
/api/1
) 發出后,還未完成。 -
請求 2 (
/api/2
) 立即發出。 -
如果請求 2 的響應比請求 1 快,那么請求 1 的響應(較慢)回來時,結果是過時的,但還是會被處理。
我們想要的:
-
當數據變化時,上一個異步請求應被取消或忽略,不再執行后續邏輯。
為了解決這個問題,Vue 提供了副作用清理機制。
副作用清理函數 (onCleanup()
)
從 Vue 3.0 開始,Vue 提供了一個清理機制,稱為 onCleanup
。
watch(id, (newId, oldId, onCleanup) => {const controller = new AbortController()fetch(`/api/${newId}`, { signal: controller.signal }).then((res) => res.json()).then((data) => {console.log('請求結果:', data)})onCleanup(() => {controller.abort() // 取消上一個請求})
})
含義解釋:
-
每次監聽的數據 (
id
) 變化時:-
先調用上一次注冊的
onCleanup
清理函數。 -
然后再執行新一次監聽回調。
-
-
因此,上一次的異步請求會自動終止,避免過時請求的結果被錯誤處理。
?watchEffect()
中的副作用清理
watchEffect((onCleanup) => {const timer = setInterval(() => {console.log('定時執行')}, 1000)onCleanup(() => {clearInterval(timer) // 清理定時器})
})
原理相同:
-
每次響應式數據變化重新執行副作用之前,先調用清理函數。
-
確保副作用(定時器、請求等)不重疊,避免內存泄漏或數據錯亂。
從 Vue 3.5 版本開始,引入了新 API:onWatcherCleanup()
:?
import { watch, onWatcherCleanup } from 'vue'watch(id, (newId) => {const controller = new AbortController()fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {console.log('請求完成:', newId)})onWatcherCleanup(() => {controller.abort() // 取消上一個請求})
})
特點:
-
不再需要第三個參數
onCleanup
。 -
可以獨立地在監聽器或
watchEffect()
回調函數內調用清理函數。
?使用限制:
-
必須在同步階段調用,不可放在
await
之后。 -
因此,必須在異步操作之前注冊。
正確用法(同步調用):watch(id, (newId) => {const controller = new AbortController()onWatcherCleanup(() => controller.abort()) // 同步調用,正確!fetch(`/api/${newId}`, { signal: controller.signal })
})? 錯誤用法(異步調用):watch(id, async (newId) => {const controller = new AbortController()await someAsyncOperation()onWatcherCleanup(() => controller.abort()) // ? 錯誤!異步調用