Skip to content

Watchers

Jednoduchý příklad

Computed proměnné nám umožňují deklarativně vypočítat odvozené hodnoty. Existují však případy, kdy v reakci na změny stavu potřebujeme provést „vedlejší efekty“ ‑ například změnu DOM nebo změnu jiné části stavu na základě výsledku asynchronní operace.

S Options API můžeme použít možnost watch k vyvolání funkce kdykoli se změní reaktivní hodnota:

js
export default {
  data() {
    return {
      question: '',
      answer: 'Otázky obvykle obsahují otazník. ;-)',
      loading: false
    }
  },
  watch: {
    // kdykoli se změní `question`, spustí se tato funkce
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = 'Přemýšlím...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Chyba! Nelze volat API. ' + error
      } finally {
        this.loading = false
      }
    }
  }
}
template
<p>
  Zeptejte se na otázku s odpovědí ano/ne:
  <input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>

Vyzkoušejte si to

Konstrukce watch podporuje i cestu ke klíči vnořené proměnné pomocí tečkové notace:

js
export default {
  watch: {
    // Pozn.: pouze jednoduché cesty. Výrazy zde podporovány nejsou.
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

S Composition API můžeme použít funkci watch k vyvolání callback funkce kdykoli se změní část reaktivního stavu:

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

const question = ref('')
const answer = ref('Otázky obvykle obsahují otazník. ;-)')
const loading = ref(false)

// watch pracuje přímo nad ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = 'Přemýšlím...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Chyba! Nelze volat API. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    Zeptejte se na otázku s odpovědí ano/ne:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

Vyzkoušejte si to

Zdrojové typy pro Watch

První parametr watch může být některý z různých typů reaktivních "zdrojů": může to být ref (vč. computed refs), reaktivní objekt, getter funkce nebo pole více různých zdrojů:

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

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

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

// pole více růzých zdrojů
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

Pamatujte, že nemůžete sledovat vlastnost reaktivního objektu tímto způsobem:

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

// toto nebude fungovat, protože do watch() předáváme pouze number
watch(obj.count, (count) => {
  console.log(`Počet je: ${count}`)
})

Místo toho použijte getter

js
// místo toho použijte getter:
watch(
  () => obj.count,
  (count) => {
    console.log(`Počet je: ${count}`)
  }
)

Deep Watchers

Výsledek watch je ve výchozím nastavení mělký (shallow): callback funkce je vyvolána pouze tehdy, když je nová hodnota přiřazena sledované vlastnosti – nespustí se při změnách vnořených vlastností. Pokud chcete, aby se callback funkce spustila i u všech vnořených změn, musíte použít tzv. deep watcher:

js
export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // Pozn.: `newValue` zde při vnořených změnách bude rovna `oldValue`,
        // dokud nebude nahrazen samotný objekt
      },
      deep: true
    }
  }
}

Když zavoláte watch() přímo na reaktivní objekt, implicitně vytvoří tzv. deep watcher ‑ callback funkce bude vyvolána i při všech změnách vnořených vlastností:

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

watch(obj, (newValue, oldValue) => {
  // bude spuštěno při změnách vnořených vlastností
  // Pozn.: `newValue` zde bude rovna `oldValue`
  // protože obě ukazují na ten samý objekt!
})

obj.count++

To by se mělo odlišovat getter funkcí, která vrací reaktivní objekt – v druhém případě se callback funkce spustí pouze v případě, že getter vrátí jiný objekt:

js
watch(
  () => state.someObject,
  () => {
    // bude spuštěno pouze pokud je nahrazen state.someObject
  }
)

Můžete nicméně donutit i druhý případ, aby se z něj stal deep watcher, explicitním použitím nastavení deep:

js
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // Pozn.: `newValue` zde bude rovna `oldValue`,
    // dokud nebude nahrazen state.someObject
  },
  { deep: true }
)

Ve Vue 3.5+ může být hodnota deep také číslo indikující maximální hloubku prohledávání - např. do kolikáté úrovně má Vue procházet vnořené vlastnosti objektu.

Používejte s rozvahou

Deep watcher vyžaduje procházení všech vnořených vlastností ve sledovaném objektu, což může být výpočetně náročné, pokud je použito na velkých datových strukturách. Používejte jej pouze v případě potřeby a dávejte pozor na důsledky pro výkon aplikace.

Eager Watchers

Výsledek watch je ve výchozím nastavení „lazy“: callback funkce není spuštěna, dokud se sledovaný zdroj nezmění. V některých případech však můžeme chtít, aby byla stejná logika callback funkce spouštěna v „eager“ módu - například můžeme chtít načíst některá počáteční data a poté načíst data znovu, kdykoli se změní relevantní stav.

Můžeme vynutit okamžité provedení callback funkce tím, že watcher deklarujeme pomocí objektu s funkcí handler a s volbou immediate: true:

js
export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // toto bude spuštěno ihned po vytvoření instance komponenty
      },
      // vynutí "eager" spuštění callback funkce
      immediate: true
    }
  }
  // ...
}

Úvodní spuštění handler funkce proběhne těsně před lifecycle hookem created. Vue již bude mít zpracovaný obsah možností data, computed a methods, takže jejich výsledky budou při tomto prvním volání dostupné.

Můžeme vynutit okamžité provedení callback funkce předáním parametru immediate: true:

js
watch(
  source, 
  (newValue, oldValue) => {
    // bude spuštěno okamžitě a poté kdykoli se změní `source`
  }, 
  { immediate: true }
)

Jednorázové watchery

  • Podporováno až od verze 3.4+

Callback watcheru bude spuštěn kdykoli, když se změní sledovaný zdroj. Pokud ho po změně zdroje chcete spustit puze jednou, použijte nastavení once: true.

js
export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // když se změní `source`, bude spuštěno pouze jednou
      },
      once: true
    }
  }
}
js
watch(
  source,
  (newValue, oldValue) => {
    // když se změní `source`, bude spuštěno pouze jednou
  },
  { once: true }
)

watchEffect()

Pro watcher callback funkci je běžné, že používá přesně stejný reaktivní stav jako zdroj. Zvažte například následující kód, který používá watcher k načtení vzdáleného zdroje, kdykoli se změní ref 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 }
)

Zejména si všimněte, jak watcher používá todoId dvakrát, jednou jako zdroj a pak znovu uvnitř callback funkce.

Toto je možné zjednodušit pomocí watchEffect(). watchEffect() nám umožňuje automaticky sledovat reaktivní závislosti callback funkce. Výše uvedený watcher lze přepsat jako:

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

Zde se callback funkce spustí okamžitě, není třeba zadávat immediate: true. Během svého vykonávání bude automaticky sledovat todoId.value jako závislost (podobně jako computed proměnné). Kdykoli se todoId.value změní, callback funkce se spustí znovu. S watchEffect() již nemusíme todoId předávat explicitně jako zdrojovou hodnotu.

Můžete se podívat na tento příklad použití watchEffect() a reaktivního načítání dat v akci.

Pro příklady jako jsou tyto, pouze s jednou závislostí, je přínos watchEffect() relativně malý. Ale pro watchers, kteří mají závislostí více, odstraňuje použití watchEffect() břemeno nutnosti seznam závislostí ručně udržovat. Kromě toho, pokud potřebujete sledovat několik vlastností ve vnořené datové struktuře, watchEffect() může být efektivnější než deep watcher, protože bude sledovat pouze vlastnosti, které jsou použity v callback funkci, a nikoli rekurzivně sledovat všechny, které v objektu existují.

TIP

watchEffect sleduje závislosti pouze při svém synchronním spuštění. Při použití s asynchronní callback funkcí budou sledovány pouze vlastnosti, ke kterým se přistoupilo před prvním výskytem await.

watch vs. watchEffect

Jak watch, tak watchEffect nám umožňují reaktivně provádět operace s vedlejšími účinky na data. Jejich hlavním rozdílem je způsob, jakým sledují své reaktivní závislosti:

  • watch sleduje pouze explicitně zadaný zdroj. Nebude sledovat nic, k čemu se přistupuje uvnitř callback funkce. Kromě toho se callback funkce spustí pouze tehdy, když se zdroj skutečně změnil. watch odděluje sledování závislostí od vedlejšího efektu, což nám dává přesnější kontrolu nad tím, kdy se má callback funkce spustit.

  • watchEffect na druhou stranu kombinuje sledování závislostí a vedlejší efekt do jedné fáze. Automaticky sleduje každou reaktivní vlastnost, ke které přistupuje během svého synchronního vykonávání. Je to pohodlnější a obvykle to vede ke stručnějšímu kódu, ale jeho reaktivní závislosti jsou méně explicitní.

Čištění vedlejších efektů

Někdy můžeme uvnitř watcheru provádět vedlejší efekty, např. asynchronní požadavky:

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

Co se však stane, pokud se id změní před dokončením požadavku? Když se předchozí požadavek dokončí, bude callback pokračovat s hodnotou ID, která už je zastaralá. V ideálním případě bychom chtěli umět tento požadavek zrušit, jakmile se id změní.

Můžeme použít API funkci onWatcherCleanup() pro zaregistrování čistící funkce, která bude zavolána ve chvíli, kdy je watcher zneplatněn a má být znovu spuštěn.

js
import { watch, onWatcherCleanup } from 'vue'

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

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

  onWatcherCleanup(() => {
    // zrušit starý požadavek
    controller.abort()
  })
})
js
import { onWatcherCleanup } from 'vue'

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

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

      onWatcherCleanup(() => {
        // zrušit starý požadavek
        controller.abort()
      })
    }
  }
}

Pozor, že funkce onWatcherCleanup je podporována až ve Vue 3.5+ a musí být volána během synchronního spuštění watchEffect efektu nebo callbacku watch funkce: nemůžete ji zavolat po await výrazu uvnitř asynchronní funkce.

Alternativně lze předat onCleanup funkci jako třetí parametr callbacku watch funkce nebo jako první parametr funkce watchEffect efektu:

js
watch(id, (newId, oldId, onCleanup) => {
  // ...
  onCleanup(() => {
    // logika pro zneplatnění
  })
})

watchEffect((onCleanup) => {
  // ...
  onCleanup(() => {
    // logika pro zneplatnění
  })
})
js
export default {
  watch: {
    id(newId, oldId, onCleanup) {
      // ...
      onCleanup(() => {
        // logika pro zneplatnění
      })
    }
  }
}

To funguje i ve verzích před Vue 3.5. Navíc je onCleanup předaná jako parametr funkce navázaná na instanci watcheru, takže nepodléhá omezení pouze na synchronní volání jako onWatcherCleanup.

Časování provedení callback funkce

Když měníte reaktivní stav, může to vyvolat aktualizace Vue komponent a callback funkce u watcherů, které jste vytvořili.

Stejně jako v případě aktualizací komponent, jsou uživatelsky vytvořené watcher callback funkce organizovány do dávek, aby se předešlo duplicitním spuštěním. Například nejspíš nechceme, aby se watcher spustil tisíckrát, když synchronně přidáme tisíc prvků do sledovaného pole.

Ve výchozím nastavení jsou watcher callback funkce volány po aktualizacích komponenty rodiče (pokud nějaké jsou) a před DOM aktualizacemi komponenty, které watcher patří. To znamená, že pokud se pokusíte přistoupit k DOM této komponenty uvnitř watcher callback funkce, její DOM bude v pre-update stavu.

Post Watchers

Pokud chcete prostřednictvím watcher callback funkce získat přístup k DOM až poté, co jej Vue aktualizuje, musíte zadat volbu flush: 'post':

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

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

Post-flush watchEffect() má také zjednodušující alias watchPostEffect():

js
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* bude vykonáno až po Vue aktualizacích */
})

Synchronní watchery

Je také možné vytvořit watcher, který bude spuštěn synchronně před provedením jakýchkoli Vue aktualizací.

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

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

Synchronní watchEffect() má také zjednodušující alias watchSyncEffect():

js
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* bude vykonáno synchronně při reaktivní změně dat */
})

Používejte s rozvahou

Synchronní watchery nejsou organizovány do dávek a spouští se pokaždé, když je zjištěna reaktivní změna. Není problém je používat pro jednoduché boolean hodnoty, ale vyhněte se jejich použití na datových zdrojích, které mohou být synchronně měněny mnohokrát, například na polích.

this.$watch()

Je také možné bezpodmínečně vytvořit watcher imperativně pomocí instanční metody $watch():

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

To je užitečné, když potřebujete watcher nastavit podmíněně, nebo sledovat jen něco v reakci na interakci uživatele. Umožňuje také watcher předčasně zastavit.

Zastavení watchers

Watchers deklarované pomocí volby watch nebo instanční metody $watch() jsou automaticky zastaveny, když je odpojena komponenta, do které patří, takže se ve většině případů nemusíte o jejich zastavení sami starat.

Pro vzácné případy, kdy potřebujete watcher zastavit předtím, než se komponenta odpojí, pro to API $watch() vrací funkci:

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

// ...když už není watcher potřeba:
unwatch()

Sledovače deklarované synchronně v rámci setup() nebo <script setup> jsou vázány na instanci komponenty, do které patří, a budou automaticky zastaveny, když je komponenta odpojena. Ve většině případů se nemusíte o jejich zastavení sami starat.

Klíčem je zde to, že watcher musí být vytvořen synchronně: pokud je watcher vytvořen v asynchronní callback funkci, nebude vázán na komponentu a musí být zastaven ručně, aby se zabránilo únikům paměti (memory leaks). Zde je příklad:

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

// tento bude automaticky zastaven
watchEffect(() => {})

// ...tento nebude!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

K ručnímu zastavení watcheru použijte vrácenou obslužnou funkci. Funguje to pro watch i watchEffect:

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

// ...později, když už není watcher potřeba
unwatch()

Mějte na paměti, že by mělo být jen velmi málo případů, kdy potřebujete vytvářet watcher asynchronně, a pokud je to možné, měla by se upřednostňovat synchronní tvorba. Pokud potřebujete počkat na některá asynchronní data, můžete místo toho logiku pro watch podmínit:

js
// data, která budou načtena asynchronně
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // kód se vykoná až po načtení dat
  }
})
Watchers has loaded