跳轉到內容

監聽器

基本示例

計算屬性允許我們宣告式地計算派生值。然而,在某些情況下,我們需要根據狀態變化執行“副作用” - 例如,修改 DOM,或根據非同步操作的結果更改另一部分狀態。

使用 Options API,我們可以使用 watch 選項 來觸發函式,當響應式屬性變化時

js
export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)',
      loading: false
    }
  },
  watch: {
    // whenever question changes, this function will run
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      } finally {
        this.loading = false
      }
    }
  }
}
template
<p>
  Ask a yes/no question:
  <input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>

在遊樂場中嘗試

watch 選項還支援以點分隔的路徑作為鍵

js
export default {
  watch: {
    // Note: only simple paths. Expressions are not supported.
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

使用 Composition 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)

// watch works directly on a 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 函式,或者多個源的陣列

js
const x = ref(0)
const y = ref(0)

// single ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// array of multiple sources
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

請注意,您不能像這樣監聽響應式物件的屬性

js
const obj = reactive({ count: 0 })

// this won't work because we are passing a number to watch()
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`)
})

相反,請使用 getter

js
// instead, use a getter:
watch(
  () => obj.count,
  (count) => {
    console.log(`Count is: ${count}`)
  }
)

深度監聽器

watch 預設為淺度監聽:只有當被監聽的屬性被賦予新值時,回撥才會觸發 - 它不會在巢狀屬性變化時觸發。如果你想回調在所有巢狀變化上觸發,你需要使用深度監聽。

js
export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // Note: `newValue` will be equal to `oldValue` here
        // on nested mutations as long as the object itself
        // hasn't been replaced.
      },
      deep: true
    }
  }
}

當你直接在響應式物件上呼叫 watch() 時,它將隱式建立一個深度監聽 - 回撥將在所有巢狀變化上觸發。

js
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // fires on nested property mutations
  // Note: `newValue` will be equal to `oldValue` here
  // because they both point to the same object!
})

obj.count++

這應該與返回響應式物件的 getter 進行區分 - 在後一種情況下,只有當 getter 返回不同的物件時,回撥才會觸發。

js
watch(
  () => state.someObject,
  () => {
    // fires only when state.someObject is replaced
  }
)

然而,你可以透過顯式使用 deep 選項將第二種情況強制轉換為深度監聽。

js
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // Note: `newValue` will be equal to `oldValue` here
    // *unless* state.someObject has been replaced
  },
  { deep: true }
)

在 Vue 3.5+ 中,deep 選項也可以是一個數字,表示最大遍歷深度 - 即 Vue 應該遍歷物件巢狀屬性多少層。

謹慎使用

深度監聽需要遍歷被監聽物件的所有巢狀屬性,在大型資料結構中使用時可能會很昂貴。僅在必要時使用,並注意效能影響。

急切監聽器

watch 預設為惰性:回撥不會在被監聽源改變時呼叫。但在某些情況下,我們可能希望相同的回撥邏輯被急切執行 - 例如,我們可能想要獲取一些初始資料,然後每當相關狀態改變時重新獲取資料。

我們可以透過宣告一個帶有 handler 函式和 immediate: true 選項的物件來強制執行一個監聽器的回撥,立即執行。

js
export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // this will be run immediately on component creation.
      },
      // force eager callback execution
      immediate: true
    }
  }
  // ...
}

handler 函式的初始執行將在 created 鉤子之前發生。Vue 已經處理了 datacomputedmethods 選項,所以這些屬性將在第一次呼叫中可用。

我們還可以透過傳遞 immediate: true 選項來強制執行一個監聽器的回撥,立即執行。

js
watch(
  source,
  (newValue, oldValue) => {
    // executed immediately, then again when `source` changes
  },
  { immediate: true }
)

一次性監聽器

  • 僅支援 3.4+

監聽器的回撥會在被監聽源改變時執行。如果你想回調在被源改變時只觸發一次,請使用 once: true 選項。

js
export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // when `source` changes, triggers only once
      },
      once: true
    }
  }
}
js
watch(
  source,
  (newValue, oldValue) => {
    // when `source` changes, triggers only once
  },
  { once: true }
)

watchEffect()

監聽器的回撥通常使用與源完全相同的響應式狀態。例如,考慮以下程式碼,它使用一個監聽器在 todoId ref 變化時載入遠端資源。

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() 可能比深層次監視器更高效,因為它只會跟蹤回撥中使用的屬性,而不是遞迴地跟蹤所有屬性。

提示

watchEffect 只在其 同步 執行期間跟蹤依賴項。當與非同步回撥一起使用時,只有在前一個 await 呼叫之前訪問的屬性才會被跟蹤。

watchwatchEffect

watchwatchEffect 都允許我們進行反應式副作用。它們的主要區別在於它們跟蹤反應式依賴項的方式。

  • watch 只跟蹤顯式監視的源。它不會跟蹤回撥內部訪問的內容。此外,回撥僅在源實際更改時觸發。watch 將依賴項跟蹤與副作用分開,使我們能夠更精確地控制回撥何時觸發。

  • 另一方面,watchEffect 將依賴項跟蹤和副作用合併為一個階段。它將自動跟蹤其在同步執行期間訪問的每個反應式屬性。這更方便,通常會產生更簡潔的程式碼,但使得其反應式依賴項不夠明確。

副作用清理

有時我們可能在監視器中執行副作用,例如非同步請求。

js
watch(id, (newId) => {
  fetch(`/api/${newId}`).then(() => {
    // callback logic
  })
})
js
export default {
  watch: {
    id(newId) {
      fetch(`/api/${newId}`).then(() => {
        // callback logic
      })
    }
  }
}

但如果在請求完成之前 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(() => {
    // callback logic
  })

  onWatcherCleanup(() => {
    // abort stale request
    controller.abort()
  })
})
js
import { onWatcherCleanup } from 'vue'

export default {
  watch: {
    id(newId) {
      const controller = new AbortController()

      fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
        // callback logic
      })

      onWatcherCleanup(() => {
        // abort stale request
        controller.abort()
      })
    }
  }
}

請注意,onWatcherCleanup 僅在 Vue 3.5+ 中受支援,並且必須在 watchEffect 效果函式或 watch 回撥函式的同步執行期間呼叫:您不能在非同步函式中的 await 語句之後呼叫它。

或者,一個 onCleanup 函式也被作為第三個引數傳遞給監視器回撥,並且作為第一個引數傳遞給 watchEffect 效果函式

js
watch(id, (newId, oldId, onCleanup) => {
  // ...
  onCleanup(() => {
    // cleanup logic
  })
})

watchEffect((onCleanup) => {
  // ...
  onCleanup(() => {
    // cleanup logic
  })
})
js
export default {
  watch: {
    id(newId, oldId, onCleanup) {
      // ...
      onCleanup(() => {
        // cleanup logic
      })
    }
  }
}

這在版本 3.5 之前有效。此外,透過函式引數傳遞的 onCleanup 繫結到監視器例項,因此它不受 onWatcherCleanup 的同步約束。

回撥重新整理時機

當您修改反應式狀態時,它可能會觸發 Vue 元件更新和您建立的監視器回撥。

類似於元件更新,使用者建立的監視器回撥被批次處理以避免重複呼叫。例如,如果我們同步地將一千個專案推送到被監視的陣列中,我們可能不希望監視器觸發一千次。

預設情況下,監視器的回撥函式將在父元件更新(如果有)之後、以及所屬元件的DOM更新之前被呼叫。這意味著如果您在監視器回撥函式中嘗試訪問所屬元件的自身DOM,DOM將處於更新前的狀態。

後置監視

如果您想在Vue更新所屬元件的DOM之後在監視器回撥中訪問所屬元件的DOM,您需要指定flush: 'post'選項

js
export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'post'
    }
  }
}
js
watch(source, callback, {
  flush: 'post'
})

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

後置重新整理watchEffect()還有一個便利別名,watchPostEffect()

js
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* executed after Vue updates */
})

同步監視

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

js
export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'sync'
    }
  }
}
js
watch(source, callback, {
  flush: 'sync'
})

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

同步watchEffect()還有一個便利別名,watchSyncEffect()

js
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* executed synchronously upon reactive data change */
})

謹慎使用

同步監視器沒有批處理,並且在檢測到每次響應式變異時都會觸發。它們可以用來監視簡單的布林值,但避免在可能同步多次變異的資料來源上使用,例如陣列。

this.$watch()

還可以使用$watch()例項方法顯式地建立監視器

js
export default {
  created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }
}

這在需要條件性地設定監視器,或者僅對使用者互動做出響應時非常有用。它還允許您提前停止監視器。

停止監視器

使用watch選項或$watch()例項方法宣告的監視器將在所屬元件解除安裝時自動停止,所以在大多數情況下您不需要擔心手動停止監視器。

在極少數需要在該元件解除安裝之前停止監視器的情況下,$watch() API將返回一個用於此目的的函式

js
const unwatch = this.$watch('foo', callback)

// ...when the watcher is no longer needed:
unwatch()

setup()<script setup>內部同步宣告的監視器繫結到所屬元件例項,並在所屬元件解除安裝時自動停止。在大多數情況下,您不需要擔心手動停止監視器。

關鍵在於監視器必須同步建立:如果監視器在非同步回撥中建立,它將不會繫結到所屬元件,並且必須手動停止以避免記憶體洩漏。以下是一個示例

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

// this one will be automatically stopped
watchEffect(() => {})

// ...this one will not!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

要手動停止監視器,請使用返回的控制代碼函式。這對於watchwatchEffect都適用

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

// ...later, when no longer needed
unwatch()

請注意,在極少數情況下需要建立非同步監視器,並且儘可能優先使用同步建立。如果您需要等待一些非同步資料,您可以將您的監視邏輯條件化

js
// data to be loaded asynchronously
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // do something when data is loaded
  }
})
觀察者已載入