監聽器
基本示例
計算屬性允許我們宣告式地計算派生值。然而,在某些情況下,我們需要根據狀態變化執行“副作用” - 例如,修改 DOM,或根據非同步操作的結果更改另一部分狀態。
使用 Composition API,我們可以使用 watch
函式 來觸發回撥,當任何響應式狀態變化時
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
監聽源型別
watch
的第一個引數可以是不同型別的響應式“源”:它可以是一個 ref(包括計算 ref)、一個響應式物件、一個 getter 函式,或者多個源的陣列
js
const x = ref(0)
const y = ref(0)
// single ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// array of multiple sources
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
請注意,您不能像這樣監聽響應式物件的屬性
js
const obj = reactive({ count: 0 })
// this won't work because we are passing a number to watch()
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})
相反,請使用 getter
js
// instead, use a getter:
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
深度監聽器
當你直接在響應式物件上呼叫 watch()
時,它將隱式建立一個深度監聽 - 回撥將在所有巢狀變化上觸發。
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// fires on nested property mutations
// Note: `newValue` will be equal to `oldValue` here
// because they both point to the same object!
})
obj.count++
這應該與返回響應式物件的 getter 進行區分 - 在後一種情況下,只有當 getter 返回不同的物件時,回撥才會觸發。
js
watch(
() => state.someObject,
() => {
// fires only when state.someObject is replaced
}
)
然而,你可以透過顯式使用 deep
選項將第二種情況強制轉換為深度監聽。
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Note: `newValue` will be equal to `oldValue` here
// *unless* state.someObject has been replaced
},
{ deep: true }
)
在 Vue 3.5+ 中,deep
選項也可以是一個數字,表示最大遍歷深度 - 即 Vue 應該遍歷物件巢狀屬性多少層。
謹慎使用
深度監聽需要遍歷被監聽物件的所有巢狀屬性,在大型資料結構中使用時可能會很昂貴。僅在必要時使用,並注意效能影響。
急切監聽器
watch
預設為惰性:回撥不會在被監聽源改變時呼叫。但在某些情況下,我們可能希望相同的回撥邏輯被急切執行 - 例如,我們可能想要獲取一些初始資料,然後每當相關狀態改變時重新獲取資料。
我們還可以透過傳遞 immediate: true
選項來強制執行一個監聽器的回撥,立即執行。
js
watch(
source,
(newValue, oldValue) => {
// executed immediately, then again when `source` changes
},
{ immediate: true }
)
一次性監聽器
- 僅支援 3.4+
監聽器的回撥會在被監聽源改變時執行。如果你想回調在被源改變時只觸發一次,請使用 once: true
選項。
js
watch(
source,
(newValue, oldValue) => {
// when `source` changes, triggers only once
},
{ once: true }
)
watchEffect()
監聽器的回撥通常使用與源完全相同的響應式狀態。例如,考慮以下程式碼,它使用一個監聽器在 todoId
ref 變化時載入遠端資源。
js
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
,一次作為源,然後再次在回撥中。
這可以透過 watchEffect()
簡化。 watchEffect()
允許我們自動跟蹤回撥的響應式依賴。上面的監聽器可以重寫為
js
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
在這裡,回撥將立即執行,無需指定 immediate: true
。在其執行過程中,它將自動跟蹤 todoId.value
作為依賴(類似於計算屬性)。每當 todoId.value
變化時,回撥將再次執行。使用 watchEffect()
,我們不再需要顯式地傳遞 todoId
作為源值。
你可以檢視 這個 watchEffect()
和響應式資料獲取的例子。
對於只有一個依賴項的示例,使用 watchEffect()
的好處相對較小。但對於具有多個依賴項的監視器,使用 watchEffect()
可以消除手動維護依賴項列表的負擔。此外,如果您需要在巢狀資料結構中監視多個屬性,watchEffect()
可能比深層次監視器更高效,因為它只會跟蹤回撥中使用的屬性,而不是遞迴地跟蹤所有屬性。
提示
watchEffect
只在其 同步 執行期間跟蹤依賴項。當與非同步回撥一起使用時,只有在前一個 await
呼叫之前訪問的屬性才會被跟蹤。
watch
與 watchEffect
watch
和 watchEffect
都允許我們進行反應式副作用。它們的主要區別在於它們跟蹤反應式依賴項的方式。
watch
只跟蹤顯式監視的源。它不會跟蹤回撥內部訪問的內容。此外,回撥僅在源實際更改時觸發。watch
將依賴項跟蹤與副作用分開,使我們能夠更精確地控制回撥何時觸發。另一方面,
watchEffect
將依賴項跟蹤和副作用合併為一個階段。它將自動跟蹤其在同步執行期間訪問的每個反應式屬性。這更方便,通常會產生更簡潔的程式碼,但使得其反應式依賴項不夠明確。
副作用清理
有時我們可能在監視器中執行副作用,例如非同步請求。
js
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// callback logic
})
})
但如果在請求完成之前 id
發生了變化怎麼辦?當先前的請求完成時,它仍然會使用已經過時的 ID 值觸發回撥。理想情況下,我們希望在 id
變為新值時取消過時的請求。
我們可以使用 onWatcherCleanup()
API 來註冊一個清理函式,該函式將在監視器被無效化和即將重新執行時被呼叫
js
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// callback logic
})
onWatcherCleanup(() => {
// abort stale request
controller.abort()
})
})
請注意,onWatcherCleanup
僅在 Vue 3.5+ 中受支援,並且必須在 watchEffect
效果函式或 watch
回撥函式的同步執行期間呼叫:您不能在非同步函式中的 await
語句之後呼叫它。
或者,一個 onCleanup
函式也被作為第三個引數傳遞給監視器回撥,並且作為第一個引數傳遞給 watchEffect
效果函式
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
這在版本 3.5 之前有效。此外,透過函式引數傳遞的 onCleanup
繫結到監視器例項,因此它不受 onWatcherCleanup
的同步約束。
回撥重新整理時機
當您修改反應式狀態時,它可能會觸發 Vue 元件更新和您建立的監視器回撥。
類似於元件更新,使用者建立的監視器回撥被批次處理以避免重複呼叫。例如,如果我們同步地將一千個專案推送到被監視的陣列中,我們可能不希望監視器觸發一千次。
預設情況下,監視器的回撥函式將在父元件更新(如果有)之後、以及所屬元件的DOM更新之前被呼叫。這意味著如果您在監視器回撥函式中嘗試訪問所屬元件的自身DOM,DOM將處於更新前的狀態。
後置監視
如果您想在Vue更新所屬元件的DOM之後在監視器回撥中訪問所屬元件的DOM,您需要指定flush: 'post'
選項
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
後置重新整理watchEffect()
還有一個便利別名,watchPostEffect()
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* executed after Vue updates */
})
同步監視
也可以建立一個在Vue管理的任何更新之前觸發的同步監視器
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
同步watchEffect()
還有一個便利別名,watchSyncEffect()
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* executed synchronously upon reactive data change */
})
謹慎使用
同步監視器沒有批處理,並且在檢測到每次響應式變異時都會觸發。它們可以用來監視簡單的布林值,但避免在可能同步多次變異的資料來源上使用,例如陣列。
停止監視器
在setup()
或<script setup>
內部同步宣告的監視器繫結到所屬元件例項,並在所屬元件解除安裝時自動停止。在大多數情況下,您不需要擔心手動停止監視器。
關鍵在於監視器必須同步建立:如果監視器在非同步回撥中建立,它將不會繫結到所屬元件,並且必須手動停止以避免記憶體洩漏。以下是一個示例
vue
<script setup>
import { watchEffect } from 'vue'
// this one will be automatically stopped
watchEffect(() => {})
// ...this one will not!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手動停止監視器,請使用返回的控制代碼函式。這對於watch
和watchEffect
都適用
js
const unwatch = watchEffect(() => {})
// ...later, when no longer needed
unwatch()
請注意,在極少數情況下需要建立非同步監視器,並且儘可能優先使用同步建立。如果您需要等待一些非同步資料,您可以將您的監視邏輯條件化
js
// data to be loaded asynchronously
const data = ref(null)
watchEffect(() => {
if (data.value) {
// do something when data is loaded
}
})