Watchers 這是啥啊?應該是幫助debug的好夥伴嗎?
它是一種用來監控數據變化並執行特定操作的功能。
雖然它可以間接地幫助除錯,
但它的主要目的是在數據變化時自動響應和執行代碼
計算屬性允許我們聲明式地計算衍生值。然而,有些情況下我們需要在狀態變更時執行「副作用」,例如,更改 DOM 或基於異步操作的結果更改另一個狀態。
使用 Composition API,我們可以使用 watch
函數在反應性狀態變更時觸發回調:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// watch 直接作用於 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>
你有問題要問嗎:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
watch
的第一個參數可以是不同類型的反應性「來源」:可以是 ref
(包括計算屬性),反應性物件,取值函數,或者多個來源組成的陣列:
const x = ref(0)
const y = ref(0)
// 單一 ref
watch(x, (newX) => {
console.log(`x 是 ${newX}`)
})
// 取值函數
watch(
() => x.value + y.value,
(sum) => {
console.log(`x + y 的和是:${sum}`)
}
)
// 多個來源的陣列
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x 是 ${newX} 而 y 是 ${newY}`)
})
請注意,您不能像下面這樣監聽反應性物件的屬性:
const obj = reactive({ count: 0 })
// 這樣不行,因為我們傳遞了一個數字給 watch()
watch(obj.count, (count) => {
console.log(`Count 是:${count}`)
})
當我們使用 reactive
創建一個反應性物件時,例如 const obj = reactive({ count: 0 })
,它的屬性(如 count
)會被 Vue 追蹤,從而使我們可以響應它們的變化。
然而,直接將 obj.count
作為 watch
的第一個參數是無效的。這是因為 obj.count
會立即被求值並返回一個數字(如 0
),而 watch
期望的是一個反應性來源,而不是一個靜態的值。
取而代之的是使用取值函數:
// 使用取值函數:
watch(
() => obj.count,
(count) => {
console.log(`Count 是:${count}`)
}
)
在這段代碼中,我們傳遞了一個箭頭函數 () => obj.count
給 watch
。這個函數返回 obj.count
的值,而不是立即求值。這樣,Vue 可以監聽 obj.count
的變化,並在它改變時觸發回調函數 count => { console.log(
Count 是:${count}) }
。這樣我們就實現了對 obj.count
變化的監聽。
當你直接在反應性物件上調用 watch()
時,它會隱式地創建一個深層監聽器 ( 回調函數將會在所有巢狀變更時觸發 ):
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在巢狀屬性變更時觸發
// 注意:這裡的 `newValue` 和 `oldValue` 會相等
// 因為它們都指向同一個物件!
})
obj.count++
這應該與返回反應性物件的取值函數有所區別 ( 在後者的情況下,回調函數僅在取值函數返回一個不同的物件時才會觸發 ) :
watch(
() => state.someObject,
() => {
// 只有當 state.someObject 被替換時才會觸發
}
)
然而,你可以通過顯式地使用 deep
選項,將第二種情況強制為深層監聽器:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:這裡的 `newValue` 和 `oldValue` 會相等
// 除非 state.someObject 被替換
},
{ deep: true }
)
在 Vue 3.5+ 中,deep
選項也可以是一個數字,表示最大遍歷深度——也就是 Vue 應該遍歷物件巢狀屬性的層級數。
深層監聽需要遍歷監聽物件的所有巢狀屬性,對於大型數據結構來說,這可能會非常耗費資源。僅在必要時使用,並注意其對性能的影響。
watch
預設是延遲執行的:回調函數不會在被監聽的來源發生變更之前執行。但在某些情況下,我們可能希望立即執行相同的回調邏輯。例如,我們可能希望獲取一些初始數據,然後在相關狀態變更時重新獲取數據。
我們可以通過傳遞 immediate: true
選項,強制監聽器的回調函數立即執行:
watch(
source,
(newValue, oldValue) => {
// 立即執行,然後在 `source` 變更時再次執行
},
{ immediate: true }
)
僅支援於 Vue 3.4+。
監聽器的回調函數會在被監聽的來源變更時執行。如果你希望回調函數在來源變更時僅觸發一次,可以使用 once: true
選項。
watch(
source,
(newValue, oldValue) => {
// 當 `source` 變更時,僅觸發一次
},
{ once: true }
)
watchEffect()
在使用監聽器回調時,通常會使用相同的響應式狀態作為來源。例如,考慮以下使用監聽器在 todoId
ref 變更時加載遠程資源的代碼:
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()
允許我們自動跟踪回調的響應式依賴。上面的監聽器可以重寫為:
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
vs. watchEffect
watch
和 watchEffect
都允許我們響應性地執行副作用。它們之間的主要區別在於跟踪響應式依賴的方式:
watch
只會跟踪顯式監聽的來源。它不會跟踪回調中訪問的任何內容。此外,只有當來源實際變更時,回調才會觸發。watch
將依賴項跟踪與副作用分開,讓我們對回調觸發的時間有更精確的控制。watchEffect
則將依賴項跟踪和副作用
結合到一個階段。它會自動跟踪其同步執行期間訪問的每個響應式屬性。這更方便,通常會產生更簡潔的代碼,但使其響應式依賴項不那麼明確。有時,我們可能會在監聽器中執行副作用,例如異步請求:
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// 回調邏輯
})
})
但如果 id
在請求完成前變更了怎麼辦?當之前的請求完成時,它仍然會使用已經過時的 ID 值觸發回調。理想情況下,我們希望能夠在 id
變更為新值時取消過時的請求。
我們可以使用 onWatcherCleanup()
3.5+ API 註冊一個清理函數,該函數會在監聽器失效並即將重新運行時被調用:
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
效果函數:
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// 清理邏輯
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// 清理邏輯
})
})
這種方法在 3.5 版本之前也適用。此外,通過函數參數傳遞的 onCleanup
綁定到監聽器實例,因此它不受 onWatcherCleanup
的同步限制。
當你修改反應性狀態時,這可能會觸發 Vue 組件更新和你創建的監聽器回調。
與組件更新類似,為了避免重複調用,使用者創建的監聽器回調會被批處理。例如,如果我們同步地將一千個項目推入一個被監聽的數組中,我們大概不希望監聽器被調用一千次。
預設情況下,監聽器的回調會在父組件更新(如果有的話)之後,在所有者組件的 DOM 更新之前被調用。這意味著如果你嘗試在監聽器回調內訪問所有者組件的 DOM,這些 DOM 會處於更新前的狀態。
如果你希望在 Vue 更新所有者組件的 DOM 後再在監聽器回調內訪問它,你需要指定 flush: 'post'
選項:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
後置刷新的 watchEffect()
也有一個方便的別名 watchPostEffect()
:
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新之後執行 */
})
也可以創建一個在任何 Vue 管理的更新之前同步觸發的監聽器:
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
同步的 watchEffect()
也有一個方便的別名 watchSyncEffect()
:
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在反應性數據變更時同步執行 */
})
同步監聽器沒有批處理,並且每次檢測到反應性變更時都會觸發。可以使用它們來監聽簡單的布林值,但避免用於可能被多次同步修改的數據源,例如數組。
在 setup()
或 <script setup>
中同步聲明的監聽器會綁定到所有者組件實例,並在所有者組件卸載時自動停止。在大多數情況下,你不需要自己擔心停止監聽器。
關鍵在於監聽器必須同步創建:如果監聽器在異步回調中創建,它將不會綁定到所有者組件,並且必須手動停止以避免記憶體洩漏。以下是示例:
<script setup>
import { watchEffect } from 'vue'
// 這個會被自動停止
watchEffect(() => {})
// ...這個則不會!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手動停止監聽器,使用返回的處理函數。這對 watch
和 watchEffect
都適用:
const unwatch = watchEffect(() => {})
// ...稍後,當不再需要時
unwatch()
注意,創建異步監聽器的情況應該非常少見,並且應優先選擇同步創建。如果你需要等待一些異步數據,你可以將監聽邏輯設為條件式:
// 異步加載的數據
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 當數據加載完成時做些什麼
}
})
老實說看完這個章節,不知道要幹嘛?實務上?
另外副作用是啥啊?!馬上再來請教AI老師~
在 Vue.js 中,watch
是用來監聽和響應數據變化的一個非常有用的工具。具體來說,它可以在以下幾種情況下應用:
watch
允許你監控某些數據屬性的變化,並在這些數據變化時執行副作用(side effects)。例如,當一個用戶輸入數據時,你可以根據輸入值進行搜索或過濾操作。watch
的回調函數中,你可以進行這些異步操作。watch
提供了在數據變化時進行這些操作的機會。watch
也提供了這樣的功能。例如,使用 onCleanup
或 onWatcherCleanup
來取消未完成的請求或清理資源。const inputText = ref('')
watch(inputText, (newText) => {
localStorage.setItem('savedText', newText)
})
const selectedCategory = ref('')
watch(selectedCategory, async (category) => {
const response = await fetch(`/api/categories/${category}`)
const items = await response.json()
itemsList.value = items
})
import { useRoute } from 'vue-router'
const route = useRoute()
watch(() => route.path, (newPath) => {
console.log(`Route changed to: ${newPath}`)
})
const userId = ref(1)
const userData = ref(null)
watch(userId, async (id) => {
const response = await fetch(`/api/users/${id}`)
userData.value = await response.json()
})
總的來說,watch
在 Vue.js 中是用來處理和響應數據變化的關鍵工具,它使得你可以輕鬆地管理副作用和異步操作,並且可以靈活地根據應用的需求來處理各種複雜的情況。
在編程中,副作用(side effects)指的是函數或操作在計算或執行過程中對程序的外部狀態所做的變更。這些變更可以是:
let count = 0;
function increment() {
count += 1; // 修改了外部變量 count
}
async function fetchData(url) {
const response = await fetch(url); // 進行網絡請求
const data = await response.json();
return data;
}
function updateElementText(elementId, text) {
const element = document.getElementById(elementId);
element.textContent = text; // 修改了 DOM 元素的內容
}
在 Vue.js 中,副作用通常指的是對 Vue 組件內部狀態或外部系統的操作
,例如:
Vue.js 提供了 watch
和 watchEffect
來處理這些副作用,幫助你在數據變化時執行相應的操作,並且可以在適當的時候清理副作用。這樣可以確保應用程序在數據變化時能夠做出正確的響應,並保持界面和狀態的同步。