如果您有疑問,請觀看視頻教程《Vue3實戰教程》
偵聽器?
基本示例?
計算屬性允許我們聲明性地計算衍生值。然而在有些情況下,我們需要在狀態變化時執行一些“副作用”:例如更改 DOM,或是根據異步操作的結果去修改另一處的狀態。
在組合式 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)// 可以直接偵聽一個 ref
watch(question, async (newQuestion, oldQuestion) => {if (newQuestion.includes('?')) {loading.value = trueanswer.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 (包括計算屬性)、一個響應式對象、一個?getter 函數、或多個數據源組成的數組:
js
const x = ref(0)
const y = ref(0)// 單個 ref
watch(x, (newX) => {console.log(`x is ${newX}`)
})// getter 函數
watch(() => x.value + y.value,(sum) => {console.log(`sum of x + y is: ${sum}`)}
)// 多個來源組成的數組
watch([x, () => y.value], ([newX, newY]) => {console.log(`x is ${newX} and y is ${newY}`)
})
注意,你不能直接偵聽響應式對象的屬性值,例如:
js
const obj = reactive({ count: 0 })// 錯誤,因為 watch() 得到的參數是一個 number
watch(obj.count, (count) => {console.log(`Count is: ${count}`)
})
這里需要用一個返回該屬性的 getter 函數:
js
// 提供一個 getter 函數
watch(() => obj.count,(count) => {console.log(`Count is: ${count}`)}
)
深層偵聽器?
直接給?watch()
?傳入一個響應式對象,會隱式地創建一個深層偵聽器——該回調函數在所有嵌套的變更時都會被觸發:
js
const obj = reactive({ count: 0 })watch(obj, (newValue, oldValue) => {// 在嵌套的屬性變更時觸發// 注意:`newValue` 此處和 `oldValue` 是相等的// 因為它們是同一個對象!
})obj.count++
相比之下,一個返回響應式對象的 getter 函數,只有在返回不同的對象時,才會觸發回調:
js
watch(() => state.someObject,() => {// 僅當 state.someObject 被替換時觸發}
)
你也可以給上面這個例子顯式地加上?deep
?選項,強制轉成深層偵聽器:
js
watch(() => state.someObject,(newValue, oldValue) => {// 注意:`newValue` 此處和 `oldValue` 是相等的// *除非* state.someObject 被整個替換了},{ deep: true }
)
在 Vue 3.5+ 中,deep
?選項還可以是一個數字,表示最大遍歷深度——即 Vue 應該遍歷對象嵌套屬性的級數。
謹慎使用
深度偵聽需要遍歷被偵聽對象中的所有嵌套的屬性,當用于大型數據結構時,開銷很大。因此請只在必要時才使用它,并且要留意性能。
即時回調的偵聽器?
watch
?默認是懶執行的:僅當數據源變化時,才會執行回調。但在某些場景中,我們希望在創建偵聽器時,立即執行一遍回調。舉例來說,我們想請求一些初始數據,然后在相關狀態更改時重新請求數據。
我們可以通過傳入?immediate: true
?選項來強制偵聽器的回調立即執行:
js
watch(source,(newValue, oldValue) => {// 立即執行,且當 `source` 改變時再次執行},{ immediate: true }
)
一次性偵聽器?
- 僅支持 3.4 及以上版本
每當被偵聽源發生變化時,偵聽器的回調就會執行。如果希望回調只在源變化時觸發一次,請使用?once: true
?選項。
js
watch(source,(newValue, oldValue) => {// 當 `source` 變化時,僅觸發一次},{ once: true }
)
watchEffect()
?
偵聽器的回調使用與源完全相同的響應式狀態是很常見的。例如下面的代碼,在每當?todoId
?的引用發生變化時使用偵聽器來加載一個遠程資源:
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()
?可能會比深度偵聽器更有效,因為它將只跟蹤回調中被使用到的屬性,而不是遞歸地跟蹤所有的屬性。
TIP
watchEffect
?僅會在其同步執行期間,才追蹤依賴。在使用異步回調時,只有在第一個?await
?正常工作前訪問到的屬性才會被追蹤。
watch
?vs.?watchEffect
?
watch
?和?watchEffect
?都能響應式地執行有副作用的回調。它們之間的主要區別是追蹤響應式依賴的方式:
-
watch
?只追蹤明確偵聽的數據源。它不會追蹤任何在回調中訪問到的東西。另外,僅在數據源確實改變時才會觸發回調。watch
?會避免在發生副作用時追蹤依賴,因此,我們能更加精確地控制回調函數的觸發時機。 -
watchEffect
,則會在副作用發生期間追蹤依賴。它會在同步執行過程中,自動追蹤所有能訪問到的響應式屬性。這更方便,而且代碼往往更簡潔,但有時其響應性依賴關系會不那么明確。
副作用清理?
有時我們可能會在偵聽器中執行副作用,例如異步請求:
js
watch(id, (newId) => {fetch(`/api/${newId}`).then(() => {// 回調邏輯})
})
但是如果在請求完成之前?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(() => {// 回調邏輯})onWatcherCleanup(() => {// 終止過期請求controller.abort()})
})
請注意,onWatcherCleanup
?僅在 Vue 3.5+ 中支持,并且必須在?watchEffect
?效果函數或?watch
?回調函數的同步執行期間調用:你不能在異步函數的?await
?語句之后調用它。
作為替代,onCleanup
?函數還作為第三個參數傳遞給偵聽器回調,以及?watchEffect
?作用函數的第一個參數:
js
watch(id, (newId, oldId, onCleanup) => {// ...onCleanup(() => {// 清理邏輯})
})watchEffect((onCleanup) => {// ...onCleanup(() => {// 清理邏輯})
})
這在 3.5 之前的版本有效。此外,通過函數參數傳遞的?onCleanup
?與偵聽器實例相綁定,因此不受?onWatcherCleanup
?的同步限制。
回調的觸發時機?
當你更改了響應式狀態,它可能會同時觸發 Vue 組件更新和偵聽器回調。
類似于組件更新,用戶創建的偵聽器回調函數也會被批量處理以避免重復調用。例如,如果我們同步將一千個項目推入被偵聽的數組中,我們可能不希望偵聽器觸發一千次。
默認情況下,偵聽器回調會在父組件更新 (如有)?之后、所屬組件的 DOM 更新之前被調用。這意味著如果你嘗試在偵聽器回調中訪問所屬組件的 DOM,那么 DOM 將處于更新前的狀態。
Post Watchers?
如果想在偵聽器回調中能訪問被 Vue 更新之后的所屬組件的 DOM,你需要指明?flush: 'post'
?選項:
js
watch(source, callback, {flush: 'post'
})watchEffect(callback, {flush: 'post'
})
后置刷新的?watchEffect()
?有個更方便的別名?watchPostEffect()
:
js
import { watchPostEffect } from 'vue'watchPostEffect(() => {/* 在 Vue 更新后執行 */
})
同步偵聽器?
你還可以創建一個同步觸發的偵聽器,它會在 Vue 進行任何更新之前觸發:
js
watch(source, callback, {flush: 'sync'
})watchEffect(callback, {flush: 'sync'
})
同步觸發的?watchEffect()
?有個更方便的別名?watchSyncEffect()
:
js
import { watchSyncEffect } from 'vue'watchSyncEffect(() => {/* 在響應式數據變化時同步執行 */
})
謹慎使用
同步偵聽器不會進行批處理,每當檢測到響應式數據發生變化時就會觸發。可以使用它來監視簡單的布爾值,但應避免在可能多次同步修改的數據源 (如數組) 上使用。
停止偵聽器?
在?setup()
?或?<script setup>
?中用同步語句創建的偵聽器,會自動綁定到宿主組件實例上,并且會在宿主組件卸載時自動停止。因此,在大多數情況下,你無需關心怎么停止一個偵聽器。
一個關鍵點是,偵聽器必須用同步語句創建:如果用異步回調創建一個偵聽器,那么它不會綁定到當前組件上,你必須手動停止它,以防內存泄漏。如下方這個例子:
vue
<script setup>
import { watchEffect } from 'vue'// 它會自動停止
watchEffect(() => {})// ...這個則不會!
setTimeout(() => {watchEffect(() => {})
}, 100)
</script>
要手動停止一個偵聽器,請調用?watch
?或?watchEffect
?返回的函數:
js
const unwatch = watchEffect(() => {})// ...當該偵聽器不再需要時
unwatch()
注意,需要異步創建偵聽器的情況很少,請盡可能選擇同步創建。如果需要等待一些異步數據,你可以使用條件式的偵聽邏輯:
js
// 需要異步請求得到的數據
const data = ref(null)watchEffect(() => {if (data.value) {// 數據加載后執行某些操作...}
})