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 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>
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
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 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
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
})
})
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()
})
})
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í
})
})
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
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
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.
Zastavení watchers
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
}
})