EP12 - 監聽器

更新 發佈閱讀 1 分鐘
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 來處理這些副作用,幫助你在數據變化時執行相應的操作,並且可以在適當的時候清理副作用。這樣可以確保應用程序在數據變化時能夠做出正確的響應,並保持界面和狀態的同步。

留言
avatar-img
卡關的人生
4會員
73內容數
分享生活趣事~
卡關的人生的其他內容
2024/11/10
Vue 提供了多種動畫技術來提升應用程式的互動性,包括基於 CSS 類別的動畫、基於狀態的動畫,以及使用監視器來動畫化數值。基於類別的動畫可通過動態添加 CSS 類別來觸發,像是觸發按鈕搖動效果。基於狀態的動畫則是透過樣式綁定,根據互動動態調整元素的外觀,例如根據滑鼠位置改變背景顏色。
Thumbnail
2024/11/10
Vue 提供了多種動畫技術來提升應用程式的互動性,包括基於 CSS 類別的動畫、基於狀態的動畫,以及使用監視器來動畫化數值。基於類別的動畫可通過動態添加 CSS 類別來觸發,像是觸發按鈕搖動效果。基於狀態的動畫則是透過樣式綁定,根據互動動態調整元素的外觀,例如根據滑鼠位置改變背景顏色。
Thumbnail
2024/11/09
Web Components 是一組網頁原生 API,允許開發者創建可重複使用的自訂元素。Vue 與 Web Components 是互補的技術,Vue 支援整合和創建自訂元素。
Thumbnail
2024/11/09
Web Components 是一組網頁原生 API,允許開發者創建可重複使用的自訂元素。Vue 與 Web Components 是互補的技術,Vue 支援整合和創建自訂元素。
Thumbnail
2024/11/08
Vue 建議使用模板構建應用程式,但在需要 JavaScript 的全程式化功能時,渲染函數可派上用場。渲染函數通過 h() 函數創建 vnode,h 是 hyperscript 的簡寫,能生成 HTML 的 JavaScript。
Thumbnail
2024/11/08
Vue 建議使用模板構建應用程式,但在需要 JavaScript 的全程式化功能時,渲染函數可派上用場。渲染函數通過 h() 函數創建 vnode,h 是 hyperscript 的簡寫,能生成 HTML 的 JavaScript。
Thumbnail
看更多
你可能也想看
Thumbnail
在 vocus 與你一起探索內容、發掘靈感的路上,我們又將啟動新的冒險——vocus App 正式推出! 現在起,你可以在 iOS App Store 下載全新上架的 vocus App。 無論是在通勤路上、日常空檔,或一天結束後的放鬆時刻,都能自在沈浸在內容宇宙中。
Thumbnail
在 vocus 與你一起探索內容、發掘靈感的路上,我們又將啟動新的冒險——vocus App 正式推出! 現在起,你可以在 iOS App Store 下載全新上架的 vocus App。 無論是在通勤路上、日常空檔,或一天結束後的放鬆時刻,都能自在沈浸在內容宇宙中。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
切換頁面卡卡有很多種原因,這裡舉的例子只針對元件太大的情境。 除了想辦法拆分外,還有一個方法就是利用 Vue 的 Async Component。
Thumbnail
切換頁面卡卡有很多種原因,這裡舉的例子只針對元件太大的情境。 除了想辦法拆分外,還有一個方法就是利用 Vue 的 Async Component。
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
自訂元件生成位置顧名思義就是可以指定部分HTML區塊渲染在特定的畫面上,即使在不同組件也能把A組件內的部分畫面,展現在B組件上,以下方程式舉例。
Thumbnail
自訂元件生成位置顧名思義就是可以指定部分HTML區塊渲染在特定的畫面上,即使在不同組件也能把A組件內的部分畫面,展現在B組件上,以下方程式舉例。
Thumbnail
Vue.js是一種基於MVVM的前端JavaScript框架,類似的框架有React、Angular等。 架設環境 安裝Visual Studio Code(https://code.visualstudio.com/) 安裝Node.js(https://nodejs.org/en/
Thumbnail
Vue.js是一種基於MVVM的前端JavaScript框架,類似的框架有React、Angular等。 架設環境 安裝Visual Studio Code(https://code.visualstudio.com/) 安裝Node.js(https://nodejs.org/en/
Thumbnail
2023 Vue直播班筆記 - 動態路由Props,接續之前的一般動態路由。分為 "寫死" 及 "彈性" 兩種。
Thumbnail
2023 Vue直播班筆記 - 動態路由Props,接續之前的一般動態路由。分為 "寫死" 及 "彈性" 兩種。
Thumbnail
Vue Router 及 具名視圖,擺脫以往切換依賴 CSS display:none 跟 display:block 互相配合,有時還得搭配 z-index 來調整層級跟 opacity 透明度的麻煩,而 Vue Router 完美的解決了這棘手的問題,且能客製頁面想要呈現的擺飾。
Thumbnail
Vue Router 及 具名視圖,擺脫以往切換依賴 CSS display:none 跟 display:block 互相配合,有時還得搭配 z-index 來調整層級跟 opacity 透明度的麻煩,而 Vue Router 完美的解決了這棘手的問題,且能客製頁面想要呈現的擺飾。
Thumbnail
觀察者模式透過主題訂閱/訊息通知的機制,極度增強系統的可擴展性、靈活性以及降低組件間的耦合度。概念直觀簡單,是非常實用的設計模式。
Thumbnail
觀察者模式透過主題訂閱/訊息通知的機制,極度增強系統的可擴展性、靈活性以及降低組件間的耦合度。概念直觀簡單,是非常實用的設計模式。
Thumbnail
上次完成到基本的CRUD及權限控制,後面花了點時間把排序、分頁、圖表總覽的部分做完,其他細節是佈署上線,一般在公司內有專屬的部門處理,僅了解一下流程。
Thumbnail
上次完成到基本的CRUD及權限控制,後面花了點時間把排序、分頁、圖表總覽的部分做完,其他細節是佈署上線,一般在公司內有專屬的部門處理,僅了解一下流程。
Thumbnail
前言 Vue 是一個現代開發框架,擁有完尚的生態系,讓我們可以將須多元件客製化,做出組件,並且可重複利用,高擴充性。在開發組件時,每個組件都擁有自己的生命周期,Vue 組件會偵測每個變數值,是否有變,並且更新內容,今天要一個一個了解 Vue 的生命週期,讓大家有更多認識。 Vue 的生命週期
Thumbnail
前言 Vue 是一個現代開發框架,擁有完尚的生態系,讓我們可以將須多元件客製化,做出組件,並且可重複利用,高擴充性。在開發組件時,每個組件都擁有自己的生命周期,Vue 組件會偵測每個變數值,是否有變,並且更新內容,今天要一個一個了解 Vue 的生命週期,讓大家有更多認識。 Vue 的生命週期
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News