iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
自我挑戰組

自學vue~點亮Roadmap過程系列 第 27

vue3鍊成術第二十七天-偵聽器

  • 分享至 

  • xImage
  •  

偵聽器

基本示例

計算屬性允許我們聲明性地計算衍生值。然而在有些情況下,我們需要在狀態變化時執行一些“副作用”:例如更改 DOM,或是根據異步操作的結果去修改另一處的狀態。

在組合式 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)

// 可以直接偵聽一個 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 函數或多個源的數組:

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}`)
})

注意,你不能直接偵聽響應式對象的屬性值:

const obj = reactive({ count: 0 })

// 錯誤,因為 watch() 得到的參數是一個 number
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`)
})

這裡需要用一個返回該屬性的 getter 函數:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的屬性變更時觸發
  // 注意:`newValue` 此處和 `oldValue` 是相等的
  // 因為它們是同一個對象!
})

obj.count++

深層偵聽器

直接給 watch() 傳入一個響應式對象,會隱式地創建一個深層偵聽器——該回調函數在所有嵌套的變更時都會被觸發:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的屬性變更時觸發
  // 注意:`newValue` 此處和 `oldValue` 是相等的
  // 因為它們是同一個對象!
})

obj.count++

相比之下,一個返回響應式對象的 getter 函數,只有在返回不同的對象時,才會觸發回調:

watch(
  () => state.someObject,
  () => {
    // 僅當 state.someObject 被替換時觸發
  }
)

你也可以給上面這個例子顯式地加上 deep 選項,強制轉成深層偵聽器:

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此處和 `oldValue` 是相等的
    // *除非* state.someObject 被整個替換了
  },
  { deep: true }
)

深度偵聽需要遍歷被偵聽對象中的所有嵌套的屬性,當用於大型數據結構時,開銷很大。因此請只在必要時才使用它,並且要留意性能。

即時回調的偵聽器

watch 默認是懶執行的:僅當數據源變化時,才會執行回調。但在某些場景中,我們希望在創建偵聽器時,立即執行一遍回調。舉例來說,我們想請求一些初始數據,然後在相關狀態更改時重新請求數據。

我們可以通過傳入 immediate: true 選項來強制偵聽器的回調立即執行:

watch(
  source,
  (newValue, oldValue) => {
    // 立即執行,且當 `source` 改變時再次執行
  },
  { immediate: true }
)

一次性偵聽器

每當被偵聽源發生變化時,偵聽器的回調就會執行。如果希望回調只在源變化時觸發一次,請使用 once: true 選項。

watch(
  source,
  (newValue, oldValue) => {
    // 當 `source` 變化時,僅觸發一次
  },
  { once: true }
)

watchEffect()

偵聽器的回調使用與源完全相同的響應式狀態是很常見的。例如下面的代碼,在每當 todoId 的引用發生變化時使用偵聽器來加載一個遠程資源:

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 僅會在其同步執行期間,才追蹤依賴。在使用異步回調時,只有在第一個 await 正常工作前訪問到的屬性才會被追蹤。

watch vs. watchEffect

watch 和 watchEffect 都能響應式地執行有副作用的回調。它們之間的主要區別是追蹤響應式依賴的方式:

  • watch 只追蹤明確偵聽的數據源。它不會追蹤任何在回調中訪問到的東西。另外,僅在數據源確實改變時才會觸發回調。watch 會避免在發生副作用時追蹤依賴,因此,我們能更加精確地控制回調函數的觸發時機。

  • watchEffect,則會在副作用發生期間追蹤依賴。它會在同步執行過程中,自動追蹤所有能訪問到的響應式屬性。這更方便,而且代碼往往更簡潔,但有時其響應性依賴關係會不那麼明確。

回調的觸發時機

當你更改了響應式狀態,它可能會同時觸發 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 更新後執行 */
})

同步偵聽器

你還可以創建一個同步觸發的偵聽器,它會在 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) {
    // 數據加載後執行某些操作...
  }
})

上一篇
vue3鍊成術第二十六天-生命週期鉤子
下一篇
vue3鍊成術第二十八天-模板引用
系列文
自學vue~點亮Roadmap過程30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言