Przejdź bezpośrednio do treści

Obserwatorzy

Podstawowy przykład

Właściwości computed pozwalają nam deklaratywnie obliczać wartości pochodne. Istnieją jednak przypadki, w których musimy wykonać jakieś "skutki uboczne" w reakcji na zmiany stanu – na przykład mutując DOM lub zmieniając inny element stanu na podstawie wyniku operacji asynchronicznej.

Z Options API możemy użyć opcji watch, aby wywołać funkcję za każdym razem, gdy zmieni się właściwość reaktywna:

js
export default {
  data() {
    return {
      question: '',
      answer: 'Pytania zazwyczaj zawierają znak zapytania. ;-)',
      loading: false
    }
  },
  watch: {
    // za każdym razem, gdy pytanie ulegnie zmianie, ta funkcja zostanie uruchomiona
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = 'Myślę...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Błąd! Nie udało się połączyć z API. ' + error
      } finally {
        this.loading = false
      }
    }
  }
}
template
<p>
  Zadaj pytanie tak/nie:
  <input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>

Wypróbuj w playground

Opcja watch obsługuje również ścieżkę rozdzieloną kropkami jako klucz:

js
export default {
  watch: {
    // Uwaga: tylko proste ścieżki. Wyrażenia nie są obsługiwane.
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

Korzystając z Composition API, możemy użyć funkcji watch, aby uruchomić funkcję zwrotną, gdy zmieni się stan reaktywny:

vue
<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Pytania zazwyczaj zawierają znak zapytania. ;-)')
const loading = ref(false)

// watch działa bezpośrednio na ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = 'Myślę...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Błąd! Nie udało się połączyć z API. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    Zadaj pytanie tak/nie:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

Wypróbuj w playground

Obserwowanie typów źródłowych

Pierwszy argument watch może być różnymi typami reaktywnych „źródeł”: może to być ref (w tym i computed ref), obiekt reaktywny, funkcja getter lub tablica wielu źródeł:

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

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

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

// tablica wielu źródeł
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x to ${newX}, a y to ${newY}`)
})

Należy pamiętać, że nie można obserwować właściwości obiektu reaktywnego w ten sposób:

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

// to nie zadziała, ponieważ przekazujemy liczbę do watch()
watch(obj.count, (count) => {
  console.log(`Liczba wynosi: ${count}`)
})

Zamiast tego użyj gettera:

js
// zamiast tego użyj gettera:
watch(
  () => obj.count,
  (count) => {
    console.log(`Liczba wynosi: ${count}`)
  }
)

"Głębocy" obserwatorzy (deep watchers)

watch jest domyślnie "płytki": funkcja zwrotna (callback) zostanie wywołana tylko wtedy, gdy obserwowana właściwość otrzyma nową wartość – nie zostanie wywołana przy zmianach w zagnieżdżonych właściwościach. Jeśli chcesz, aby funkcja zwrotna była wywoływana na wszystkich zagnieżdżonych mutacjach, musisz użyć "głębokiego" obserwatora:

js
export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // Uwaga: `newValue` będzie równe `oldValue` w przypadku
        // zagnieżdżonych mutacji, dopóki sam obiekt nie zostanie
        // zastąpiony.
      },
      deep: true
    }
  }
}

Gdy wywołasz watch() bezpośrednio na reaktywnym obiekcie, zostanie automatycznie stworzony "głęboki" obserwator – funkcja zwrotna zostanie wywołana przy każdej zmianie w zagnieżdżonych właściwościach:

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

watch(obj, (newValue, oldValue) => {
  // wywołuje się przy mutacjach zagnieżdżonych właściwości
  // Uwaga: `newValue` będzie równe `oldValue`, ponieważ obydwa
  // wskazują na ten sam obiekt!
})

obj.count++

Należy to rozróżnić od getterów, które zwracają reaktywny obiekt – w takim przypadku funkcja zwrotna zostanie wywołana tylko wtedy, gdy getter zwróci inny obiekt:

js
watch(
  () => state.someObject,
  () => {
    // wywołuje się tylko wtedy, gdy state.someObject zostanie zastąpione
  }
)

Możesz jednak wymusić, aby drugi przypadek działał jako "głęboki" obserwator, używając opcji deep:

js
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // Uwaga: `newValue` będzie równe `oldValue`, chyba że
    // state.someObject zostało zastąpione
  },
  { deep: true }
)

W Vue 3.5+, opcja deep może również być również wartością liczbową określającą maksymalną głębokość przeszukiwania - np. ile poziomów w głąb danego obiektu Vue powinno go obserwować.

Używaj ostrożnie

Głębokie obserwowanie wymaga przejścia przez wszystkie zagnieżdżone właściwości w obserwowanym obiekcie i może być kosztowny w przypadku dużych struktur danych. Używaj go tylko wtedy, gdy jest to konieczne i uważaj na implikacje wydajnościowe.

Obserwatorzy natychmiastowi

watch jest domyślnie leniwy (lazy): wywołanie zwrotne nie zostanie wywołane, dopóki obserwowane źródło nie ulegnie zmianie. Jednak w niektórych przypadkach możemy chcieć, aby ta sama logika wywołania zwrotnego była uruchamiana natychmiast — na przykład, gdy chcemy pobrać początkowe dane, a następnie ponownie pobrać je, gdy stan się zmieni.

Możemy wymusić natychmiastowe wykonanie funkcji zwrotnej obserwatora, deklarując ją jako obiekt z funkcją handler oraz opcją immediate: true:

js
export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // ta funkcja zostanie uruchomiona natychmiast podczas tworzenia komponentu.
      },
      // wymusza natychmiastowe wykonanie funkcji zwrotnej
      immediate: true
    }
  }
  // ...
}

Początkowe wykonanie funkcji handlera nastąpi tuż przed wywołaniem haka created. Vue zdąży już przetworzyć opcje data, computed oraz methods, więc te właściwości będą dostępne przy pierwszym wywołaniu.

Możemy wymusić natychmiastowe wykonanie funkcji zwrotnej watchera, przekazując opcję immediate: true:

js
watch(
  source,
  (newValue, oldValue) => {
    // wykona się natychmiast, a potem ponownie, gdy `source` się zmieni
  },
  { immediate: true }
)

Obserwatorzy jednorazowi

  • Wsparcie tylko w 3.4+

Funkcja zwrotna watchera wykona się za każdym razem, gdy zmieni się obserwowane źródło. Jeśli chcesz, aby funkcja zwrotna została wywołana tylko raz, gdy źródło się zmieni, użyj opcji once: true.

js
export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // gdy `source` się zmieni, funkcja wywoła się tylko raz
      },
      once: true
    }
  }
}
js
watch(
  source,
  (newValue, oldValue) => {
    // gdy `source` się zmieni, wywoła się tylko raz
  },
  { once: true }
)

watchEffect()

Często funkcja zwrotna obserwatora używa dokładnie tego samego reaktywnego stanu co źródło. Na przykład, rozważmy poniższy kod, który używa obserwatora do załadowania zasobów zdalnych, gdy zmienia się wartość todoId:

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

W szczególności zauważ, jak watcher używa todoId dwukrotnie: raz jako źródło, a następnie ponownie wewnątrz funkcji zwrotnej.

Można to uprościć za pomocą watchEffect(). watchEffect() pozwala automatycznie śledzić zależności reaktywne funkcji zwrotnej. Powyższy watcher można zapisać w następujący sposób:

js
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

Tutaj funkcja zwrotna wykona się natychmiast, nie trzeba używać immediate: true. Podczas jej wykonywania automatycznie zostanie śledzona todoId.value jako zależność (podobnie jak w przypadku właściwości obliczanych). Za każdym razem, gdy todoId.value się zmieni, funkcja zwrotna zostanie ponownie wywołana. Dzięki watchEffect() nie musimy jawnie przekazywać todoId jako źródła wartości.

Możesz zapoznać się z tym przykładem użycia watchEffect() i reaktywnego pobierania danych w akcji.

Dla przykładów takich jak ten, z jedną zależnością, korzyści z użycia watchEffect() są stosunkowo niewielkie. Jednak w przypadku obserwatorów, które mają wiele zależności, użycie watchEffect() eliminuje konieczność ręcznego utrzymywania listy zależności. Ponadto, jeśli musisz śledzić wiele właściwości w zagnieżdżonej strukturze danych, watchEffect() może okazać się bardziej wydajny niż "głęboki" obserwator, ponieważ będzie śledził tylko te właściwości, które są używane w funkcji zwrotnej, zamiast rekurencyjnie śledzić wszystkie z nich.

TIP

watchEffect śledzi zależności tylko podczas swojego synchronicznego wykonania. Podczas używania go z asynchronicznym wywołaniem zwrotnym, śledzone będą tylko właściwości, do których uzyskano dostęp przed pierwszym znacznikiem await.

watch vs. watchEffect

watch i watchEffect pozwalają na reaktywne wykonywanie efektów ubocznych. Ich główna różnica polega na sposobie śledzenia zależności reaktywnych:

  • watch śledzi tylko jawnie obserwowane źródło. Nie śledzi niczego, co jest używane wewnątrz funkcji zwrotnej. Ponadto funkcja zwrotna wywołuje się tylko wtedy, gdy źródło faktycznie się zmieni. watch oddziela śledzenie zależności od efektów ubocznych, dając większą kontrolę nad tym, kiedy funkcja zwrotna powinna być wywołana.

  • z kolei watchEffect łączy śledzenie zależności i efekt uboczny w jednej fazie. Automatycznie śledzi każdą reaktywną właściwość dostępną podczas swojego synchronicznego wykonania. Jest to wygodniejsze i zazwyczaj skutkuje zwięzłym kodem, ale sprawia, że jego reaktywne zależności są mniej jednoznaczne.

Czyszczenie skutków ubocznych

Zdarza się, że czasem wywołujemy jakieś skutki uboczne wewnątrz obserwatorów, np. zapytania asynchroniczne:

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

Jednakże, co jeśli id ulegnie zmianie, zanim request się zakończy? Gdy poprzedni request ukończy się, wywoła funkcję zwrotną z wartością ID, która jest już stara. Idealnie, chcielibyśmy przerwać request ze starą wartością gdy id się zmieni.

Możemy użyć do tego API onWatcherCleanup() , aby zarejestrować funkcję czyszczącą, która wywoła się gdy obserwator będzie miał być zaraz wywołany z nową wartością:

js
import { watch, onWatcherCleanup } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // logika funkcji zwrotnej
  })

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

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

      fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
        // logika funkcji zwrotnej
      })

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

Pamiętaj, że onWatcherCleanup ma wsparcie w Vue 3.5+ i musi być wywołane w synchronicznym wywołaniu funkcji watchEffect lub w funkcji zwrotnej watch - nie możesz wywołać go po instrukcji await w funkcji asynchronicznej.

Alternatywnie, funkcja onCleanup może również być przekazana do obserwatora jako trzeci argument, a do funkcji watchEffect jako pierwszy argument:

js
watch(id, (newId, oldId, onCleanup) => {
  // ...
  onCleanup(() => {
    // logika czyszcząca
  })
})

watchEffect((onCleanup) => {
  // ...
  onCleanup(() => {
    // logika czyszcząca
  })
})
js
export default {
  watch: {
    id(newId, oldId, onCleanup) {
      // ...
      onCleanup(() => {
        // logika czyszcząca
      })
    }
  }
}

Działa to w wersjach przed 3.5. Dodatkowo, onCleanup przekazany jako argument funkcji jest powiązany z instancją obserwatora więc nie jest zależny od synchronicznych ograniczeń onWatcherCleanup.

Czas wywołania funkcji zwrotnej

Gdy modyfikujesz stan reaktywny, może to wywołać zarówno aktualizacje komponentów Vue, jak i funkcje zwrotne watchera, które utworzyłeś.

Podobnie jak w przypadku aktualizacji komponentów, wywołania zwrotne obserwatora tworzone przez użytkownika są grupowane, aby uniknąć duplikowania wywołań. Na przykład prawdopodobnie nie chcemy, aby obserwator uruchamiał się tysiąc razy, jeśli synchronicznie wprowadzimy tysiąc elementów do obserwowanej tablicy.

Domyślnie funkcja zwrotna obserwatora jest wywoływana po aktualizacjach komponentu nadrzędnego (jeśli takie są), a przed aktualizacjami DOM komponentu właściciela. Oznacza to, że jeśli spróbujesz uzyskać dostęp do DOM komponentu właściciela w wywołaniu zwrotnym obserwatora, DOM będzie w stanie przed aktualizacją.

Obserwatorzy post

Jeśli chcesz uzyskać dostęp do DOM komponentu właściciela w wywołaniu zwrotnym obserwatora po jego zaktualizowaniu przez Vue, musisz określić opcję flush: 'post':

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

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

Post-flush watchEffect() ma również wygodny alias, watchPostEffect():

js
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* wykonywane po aktualizacjach Vue */
})

Obserwatorzy synchroniczni

Można również utworzyć obserwator, który będzie uruchamiany synchronicznie, przed jakimikolwiek aktualizacjami zarządzanymi przez Vue:

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

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

Synchroniczny watchEffect() ma również wygodny alias, watchSyncEffect():

js
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* wykonywane synchronicznie po reaktywnej zmianie danych */
})

Używaj ostrożnie

Synchroniczni obserwatorzy nie mają "porcjowania" i wyzwalają się za każdym razem, gdy zostanie wykryta reaktywna mutacja. Można ich używać do obserwowania prostych wartości logicznych, ale należy unikać używania ich w źródłach danych, które mogą być wielokrotnie mutowane synchronicznie, takich jak tablice.

this.$watch()

Możliwe jest również imperatywne tworzenie obserwatorów za pomocą metody instancji $watch():

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

Jest to przydatne, gdy musisz warunkowo ustawić obserwator lub obserwować coś w odpowiedzi na interakcję użytkownika. Pozwala to również na wcześniejsze zatrzymanie obserwatora.

Zatrzymywanie obserwatora

Obserwatorzy zadeklarowani za pomocą opcji watch lub metody instancji $watch() są automatycznie zatrzymywani, gdy komponent właściciela jest usuwany, więc w większości przypadków nie musisz martwić się o zatrzymywanie obserwatora samodzielnie.

W rzadkich przypadkach, gdy musisz zatrzymać obserwator przed usunięciem komponentu właściciela, API $watch() zwraca w tym celu funkcję:

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

// ...gdy obserwator nie jest już potrzebny:
unwatch()

Obserwatorzy zadeklarowani synchronicznie w setup() lub <script setup> są związani z instancją komponentu właściciela i będą automatycznie zatrzymywane, gdy komponent właściciela zostanie usunięty. W większości przypadków nie musisz martwić się o zatrzymywanie obserwatora samodzielnie.

Kluczowe jest to, że obserwator musi być tworzony synchronicznie: jeśli obserwator zostanie stworzony w asynchronicznej funkcji zwrotnej, nie będzie związany z komponentem właściciela i będzie musiał zostać zatrzymany ręcznie, aby uniknąć wycieków pamięci. Oto przykład:

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

// ten będzie automatycznie zatrzymany
watchEffect(() => {})

// ...ten nie będzie!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

Aby ręcznie zatrzymać obserwator, użyj zwróconej funkcji obsługi. Działa to zarówno dla watch, jak i watchEffect:

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

// ...później, gdy nie jest już potrzebny
unwatch()

Należy zauważyć, że przypadków, w których trzeba tworzyć obserwatorów asynchronicznych, powinno być bardzo mało, a tworzenie synchroniczne powinno być preferowane, gdy tylko to możliwe. Jeśli musisz poczekać na jakieś dane asynchroniczne, możesz zamiast tego sprawić, by logika obserwatora była warunkowa:

js
// dane do załadowania asynchronicznie
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // wykonaj coś, gdy dane zostaną załadowane
  }
})
ObserwatorzyJest załadowany