2024-09-20|閱讀時間 ‧ 約 22 分鐘

EP12 - 監聽器

Watchers 這是啥啊?應該是幫助debug的好夥伴嗎?
它是一種用來監控數據變化並執行特定操作的功能。
雖然它可以間接地幫助除錯,
但它的主要目的是在數據變化時自動響應和執行代碼

基本範例 - Basic Example

計算屬性允許我們聲明式地計算衍生值。然而,有些情況下我們需要在狀態變更時執行「副作用」,例如,更改 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>

Try it in the Playground

Watch Source Types - 監聽來源類型

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.countwatch。這個函數返回 obj.count 的值,而不是立即求值。這樣,Vue 可以監聽 obj.count 的變化,並在它改變時觸發回調函數 count => { console.log(Count 是:${count}) }。這樣我們就實現了對 obj.count 變化的監聽。

Deep Watchers - 深層監聽器

當你直接在反應性物件上調用 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 應該遍歷物件巢狀屬性的層級數。

小心使用

深層監聽需要遍歷監聽物件的所有巢狀屬性,對於大型數據結構來說,這可能會非常耗費資源。僅在必要時使用,並注意其對性能的影響。

積極監聽器 - Eager Watchers​

watch 預設是延遲執行的:回調函數不會在被監聽的來源發生變更之前執行。但在某些情況下,我們可能希望立即執行相同的回調邏輯。例如,我們可能希望獲取一些初始數據,然後在相關狀態變更時重新獲取數據。

我們可以通過傳遞 immediate: true 選項,強制監聽器的回調函數立即執行:

watch(
source,
(newValue, oldValue) => {
// 立即執行,然後在 `source` 變更時再次執行
},
{ immediate: true }
)

單次監聽器 - Once Watchers​

僅支援於 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

watchwatchEffect 都允許我們響應性地執行副作用。它們之間的主要區別在於跟踪響應式依賴的方式:

  • watch 只會跟踪顯式監聽的來源。它不會跟踪回調中訪問的任何內容。此外,只有當來源實際變更時,回調才會觸發。watch 將依賴項跟踪與副作用分開,讓我們對回調觸發的時間有更精確的控制。
  • watchEffect 則將依賴項跟踪和副作用結合到一個階段。它會自動跟踪其同步執行期間訪問的每個響應式屬性。這更方便,通常會產生更簡潔的代碼,但使其響應式依賴項不那麼明確。

副作用清理 - Side Effect Cleanup

有時,我們可能會在監聽器中執行副作用,例如異步請求:

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 的同步限制。

回調刷新時機 - Callback Flush Timing​

當你修改反應性狀態時,這可能會觸發 Vue 組件更新和你創建的監聽器回調。

與組件更新類似,為了避免重複調用,使用者創建的監聽器回調會被批處理。例如,如果我們同步地將一千個項目推入一個被監聽的數組中,我們大概不希望監聽器被調用一千次。

預設情況下,監聽器的回調會在父組件更新(如果有的話)之後,在所有者組件的 DOM 更新之前被調用。這意味著如果你嘗試在監聽器回調內訪問所有者組件的 DOM,這些 DOM 會處於更新前的狀態。

後置監聽器 - Post Watchers

如果你希望在 Vue 更新所有者組件的 DOM 後再在監聽器回調內訪問它,你需要指定 flush: 'post' 選項:

watch(source, callback, {
flush: 'post'
})

watchEffect(callback, {
flush: 'post'
})

後置刷新的 watchEffect() 也有一個方便的別名 watchPostEffect()

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
/* 在 Vue 更新之後執行 */
})

同步監聽器 - Sync Watchers

也可以創建一個在任何 Vue 管理的更新之前同步觸發的監聽器:

watch(source, callback, {
flush: 'sync'
})

watchEffect(callback, {
flush: 'sync'
})

同步的 watchEffect() 也有一個方便的別名 watchSyncEffect()

import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
/* 在反應性數據變更時同步執行 */
})

謹慎使用

同步監聽器沒有批處理,並且每次檢測到反應性變更時都會觸發。可以使用它們來監聽簡單的布林值,但避免用於可能被多次同步修改的數據源,例如數組。

停止監聽器 - Stopping a Watcher​

setup()<script setup> 中同步聲明的監聽器會綁定到所有者組件實例,並在所有者組件卸載時自動停止。在大多數情況下,你不需要自己擔心停止監聽器。

關鍵在於監聽器必須同步創建:如果監聽器在異步回調中創建,它將不會綁定到所有者組件,並且必須手動停止以避免記憶體洩漏。以下是示例:

<script setup>
import { watchEffect } from 'vue'

// 這個會被自動停止
watchEffect(() => {})

// ...這個則不會!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>

要手動停止監聽器,使用返回的處理函數。這對 watchwatchEffect 都適用:

const unwatch = watchEffect(() => {})

// ...稍後,當不再需要時
unwatch()

注意,創建異步監聽器的情況應該非常少見,並且應優先選擇同步創建。如果你需要等待一些異步數據,你可以將監聽邏輯設為條件式:

// 異步加載的數據
const data = ref(null)

watchEffect(() => {
if (data.value) {
// 當數據加載完成時做些什麼
}
})
老實說看完這個章節,不知道要幹嘛?實務上?
另外副作用是啥啊?!馬上再來請教AI老師~

Q:Watch實務上應用?

在 Vue.js 中,watch 是用來監聽和響應數據變化的一個非常有用的工具。具體來說,它可以在以下幾種情況下應用:

  1. 監控數據變化
    • watch 允許你監控某些數據屬性的變化,並在這些數據變化時執行副作用(side effects)。例如,當一個用戶輸入數據時,你可以根據輸入值進行搜索或過濾操作。
  2. 執行異步操作
    • 當數據變化時,你可能需要進行異步操作,比如從 API 請求數據或提交表單。在 watch 的回調函數中,你可以進行這些異步操作。
  3. 更新其他數據
    • 你可以根據一個數據的變化來更新另一個數據。例如,當用戶選擇不同的選項時,你可能需要根據選擇的內容更新顯示的列表。
  4. 處理複雜的副作用
    • 如果你需要在數據變化時進行一些複雜的操作,如計算、數據處理、外部庫調用等,watch 提供了在數據變化時進行這些操作的機會。
  5. 清理副作用
    • 當你需要在監聽器不再需要時進行清理(如取消請求),watch 也提供了這樣的功能。例如,使用 onCleanuponWatcherCleanup 來取消未完成的請求或清理資源。

實際應用示例

  1. 自動保存用戶輸入
    const inputText = ref('')

    watch(inputText, (newText) => {
    localStorage.setItem('savedText', newText)
    })
  2. 根據用戶選擇更新顯示
    const selectedCategory = ref('')

    watch(selectedCategory, async (category) => {
    const response = await fetch(`/api/categories/${category}`)
    const items = await response.json()
    itemsList.value = items
    })
  3. 監控路由變化
    import { useRoute } from 'vue-router'

    const route = useRoute()

    watch(() => route.path, (newPath) => {
    console.log(`Route changed to: ${newPath}`)
    })
  4. 處理異步請求的副作用
    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 中是用來處理和響應數據變化的關鍵工具,它使得你可以輕鬆地管理副作用和異步操作,並且可以靈活地根據應用的需求來處理各種複雜的情況。

Q:副作用是啥?

在編程中,副作用(side effects)指的是函數或操作在計算或執行過程中對程序的外部狀態所做的變更。這些變更可以是:

  1. 修改變量
    • 例如,修改全局變量或函數外部的變量。
  2. 執行 I/O 操作
    • 例如,讀取或寫入文件,進行網絡請求,或操作本地存儲等。
  3. 更改界面
    • 例如,更新用戶界面上的內容,或者更改 HTML 元素的樣式或屬性。
  4. 觸發事件
    • 例如,觸發用戶界面的事件或調用回調函數。
  5. 操作外部系統
    • 例如,與外部 API 交互,發送郵件,或記錄日志等。

副作用的例子

  1. 函數修改變量
    let count = 0;

    function increment() {
    count += 1; // 修改了外部變量 count
    }
  2. 網絡請求
    async function fetchData(url) {
    const response = await fetch(url); // 進行網絡請求
    const data = await response.json();
    return data;
    }
  3. 更新 DOM
    function updateElementText(elementId, text) {
    const element = document.getElementById(elementId);
    element.textContent = text; // 修改了 DOM 元素的內容
    }

在 Vue.js 中的副作用

在 Vue.js 中,副作用通常指的是對 Vue 組件內部狀態或外部系統的操作,例如:

  • 異步數據加載: 在數據變化時發起 API 請求來加載或更新數據。
  • 更新組件狀態: 根據用戶輸入或其他狀態的變化更新組件的內部狀態。
  • 修改 DOM: 根據數據變化修改 DOM,例如動態顯示或隱藏元素。

Vue.js 提供了 watchwatchEffect 來處理這些副作用,幫助你在數據變化時執行相應的操作,並且可以在適當的時候清理副作用。這樣可以確保應用程序在數據變化時能夠做出正確的響應,並保持界面和狀態的同步。

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.