Skip to content

Reaktivita podrobně

Jednou z nejvýraznějších vlastností Vue je nenápadný systém reaktivity. Stav komponenty se skládá z reaktivních JavaScript objektů. Když je upravíte, zobrazení se aktualizuje. To dělá správu stavu jednodušší a intuitivní. Je však také důležité pochopit, jak to funguje, abyste se vyhnuli běžným problémům. Na této stránce se budeme zabývat některými podrobnostmi reaktivního systému Vue na nižší úrovni.

Co je reaktivita?

Tento termín se v programování často používá, ale co tím lidé myslí? Reaktivita je programovací paradigma, které nám umožňuje přizpůsobit se změnám deklarativním způsobem. Klasickým příkladem, který lidé obvykle ukazují, protože je velmi názorný, je tabulkový procesor Excel:

ABC
0
1
1
2
2
3

Zde je buňka A2 definována pomocí vzorce = A0 + A1 (kliknutím na A2 si můžete zobrazit nebo upravit vzorec), takže tabulkový procesor nám dává výsledek 3. Žádné překvapení. Když však aktualizujete A0 nebo A1, všimnete si, že A2 se také automaticky aktualizuje.

JavaScript obvykle takto nefunguje. Kdybychom chtěli napsat něco srovnatelného v JavaScriptu:

js
let A0 = 1
let A1 = 2
let A2 = A0 + A1

console.log(A2) // 3

A0 = 2
console.log(A2) // Stále 3

Když změníme A0, A2 se automaticky nezmění.

Jak bychom to tedy v JavaScriptu udělali? Abychom znovu spustili kód, který aktualizuje A2, zabalme ho nejprve do funkce:

js
let A2

function update() {
  A2 = A0 + A1
}

Potom musíme definovat několik pojmů:

  • Funkce update() produkuje vedlejší efekt (side effect), nebo zkráceně efekt, protože mění stav programu.

  • A0 a A1 jsou považovány za závislosti (dependencies) efektu, protože jejich hodnoty se používají k provedení efektu. Efekt je nazýván odběratelem (subscriber) svých závislostí.

Co potřebujeme, je magická funkce, která může vyvolat update() (efekt) pokaždé, když se změní A0 nebo A1 (závislosti):

js
whenDepsChange(update)

Tato funkce whenDepsChange() má následující úkoly:

  1. Sledovat, když je proměnná čtena. Například při vyhodnocování výrazu A0 + A1 jsou čteny obě proměnné A0 a A1.

  2. Pokud je proměnná čtena, když je právě spuštěný efekt, stane se tento efekt odběratelem této proměnné. Například protože jsou proměnné A0 a A1 čteny při vykonávání update(), po prvním volání se update() stane odběratelem obou proměnných A0 a A1.

  3. Detekovat, když se proměnná změní. Například když je proměnné A0 přiřazena nová hodnota, upozornit všechny efekty odběratelů, aby se znovu spustily.

Jak funguje reaktivita ve Vue

Ve skutečnosti nemůžeme čtení a zápis lokálních proměnných sledovat jako v příkladu. V čistém JavaScriptu pro to prostě neexistuje mechanismus. Co ale můžeme udělat, je zachytit čtení a zápis vlastností objektů.

Existují dva způsoby, jak zachytit přístup k vlastnostem v JavaScriptu: getters / settersProxies. Vue 2 používalo výhradně getters / setters kvůli omezením podpory prohlížečů. Ve Vue 3 se pro reaktivní objekty používají proxies a pro refs se používají getters / setters. Zde je pseudokód, který ilustruje, jak fungují:

js
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

TIP

Ukázky kódu zde a níže mají za úkol vysvětlit základní koncepty co nejjednodušším způsobem, takže je mnoho detailů vynecháno a okrajové případy jsou ignorovány.

Toto vysvětluje několik omezení reaktivních objektů, o kterých jsme mluvili v kapitole základů:

  • Když přiřadíte nebo destruujete vlastnost reaktivního objektu do místní proměnné, přístup nebo přiřazení k této proměnné není reaktivní, protože již nevyvolává get / set proxy traps na zdrojovém objektu. Toto „odpojení“ však ovlivňuje pouze vazbu proměnné - pokud proměnná odkazuje na neprimitivní hodnotu, jako je objekt, změna objektu reaktivní pořád bude.

  • Vrácená proxy z reactive(), i když se chová stejně jako originál, má odlišnou identitu, pokud ji porovnáme s původním objektem pomocí operátoru ===.

Uvnitř track() kontrolujeme, zda je právě spuštěný efekt. Pokud ano, vyhledáme efekty odběratelů (uložené v Set kolekci) pro sledovanou vlastnost a přidáme efekt do kolekce:

js
// Toto bude nastaveno před spuštěním efektu.
// Vrátíme se k tomu později.
let activeEffect

function track(target, key) {
  if (activeEffect) {
    const effects = getSubscribersForProperty(target, key)
    effects.add(activeEffect)
  }
}

Odběry efektů jsou uloženy ve globální datové struktuře WeakMap<target, Map<key, Set<effect>>>. Pokud nebyl nalezen Set odběratelů efektů pro vlastnost (sledovanou poprvé), bude vytvořen. To je v krátkosti to, co funkce getSubscribersForProperty() dělá. Pro jednoduchost se vyhneme jejím podrobnostem.

Uvnitř funkce trigger() pro vlastnost opět vyhledáváme efekty odběratelů. Tentokrát je však voláme:

js
function trigger(target, key) {
  const effects = getSubscribersForProperty(target, key)
  effects.forEach((effect) => effect())
}

Nyní se vraťme k funkci whenDepsChange():

js
function whenDepsChange(update) {
  const effect = () => {
    activeEffect = effect
    update()
    activeEffect = null
  }
  effect()
}

Obaluje původní funkci update v efektu, který nastavuje sám sebe jako aktuální aktivní efekt před spuštěním skutečné aktualizace. To umožňuje volání track() během aktualizace pro nalezení aktuálního aktivního efektu.

V tomto bodě jsme vytvořili efekt, který automaticky sleduje své závislosti a znovu se spouští, kdykoli se změní některá závislost. Tento efekt nazýváme Reaktivní efekt (reactive effect).

Vue poskytuje API, které vám umožňuje reaktivní efekty vytvářet: watchEffect(). Vlastně jste si možná všimli, že funguje docela podobně jako magická funkce whenDepsChange() v příkladu. Nyní můžeme přepracovat původní příklad pomocí skutečných Vue API:

js
import { ref, watchEffect } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = ref()

watchEffect(() => {
  // sleduje A0 a A1
  A2.value = A0.value + A1.value
})

// spustí efekt
A0.value = 2

Použití reaktivního efektu pro změnu ref není nejzajímavější použití - vlastně to bude názornější při použití computed proměnné:

js
import { ref, computed } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = computed(() => A0.value + A1.value)

A0.value = 2

Interně computed spravuje svou neplatnost a nový výpočet pomocí reaktivního efektu.

Takže jaký je příklad běžného a užitečného reaktivního efektu? Například aktualizace DOM! Jednoduché „reaktivní vykreslování“ můžeme implementovat takto:

js
import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  document.body.innerHTML = `počet je: ${count.value}`
})

// aktualizuje DOM
count.value++

Vlastně je to velmi podobné tomu, jak Vue komponenta udržuje synchronizaci svého stavu s DOM - každá instance komponenty vytváří reaktivní efekt pro vykreslování a aktualizaci DOM. Samozřejmě, Vue komponenty používají mnohem efektivnější způsoby aktualizace DOM než innerHTML. O nich se mluví v Mechanismu vykreslování.

API ref(), computed() a watchEffect() jsou součástí Composition API. Pokud jste dosud Vue používali pouze s Options API, zjistíte, že Composition API je blíže tomu, jak reaktivní systém Vue funguje uvnitř. Ve praxi je Options API v Vue 3 implementováno na vrstvě Composition API. Přístup k vlastnostem instance komponenty (this) spouští gettery / settery pro sledování reaktivity a volby jako watch a computed interně vyvolávají jejich ekvivalenty v Composition API.

Runtime vs. Compile-time reaktivita

Reaktivní systém Vue je převážně runtime-based: sledování a spouštění se provádí přímo v prohlížeči během spouštění kódu. Výhodou runtime reaktivity je, že může fungovat bez build fáze a je zde méně okrajových případů. Na druhou stranu je omezena syntaxí JavaScriptu, což vede k potřebě kontejnerů pro hodnoty jako jsou Vue refs.

Některé frameworky, například Svelte, se rozhodly tyto omezení překonat implementací reaktivity během kompilace. Analyzují a transformují kód, aby reaktivitu simulovaly. Kompilační krok umožňuje frameworku změnit sémantiku samotného JavaScriptu - například implicitně vkládá kód, který provádí analýzu závislostí a spouštění efektů při přístupu k lokálně definovaným proměnným. Nevýhodou je, že takové transformace vyžadují build fázi a změna sémantiky JavaScriptu v podstatě vytváří jazyk, který vypadá jako JavaScript, ale kompiluje se do něčeho jiného.

Tým Vue tuto cestu zkoumal pomocí experimentální funkce nazvané transformace reaktivity, ale nakonec jsme se rozhodli, že by to pro projekt nebylo vhodné z důvodů uvedených zde.

Ladění reaktivity

Je skvělé, že reaktivní systém Vue sleduje závislosti automaticky, ale občas se může hodit zjistit, které závislosti jsou sledovány nebo co způsobuje překreslení komponenty.

Debugging Hooks komponenty

Pomocí lifecycle hooks renderTrackedonRenderTracked a renderTriggeredonRenderTriggered můžeme ladit, které závislosti jsou během vykreslování komponenty používány a která závislost spouští aktualizaci. Oba hooks obdrží debug událost, která obsahuje informace o dané závislosti. Doporučuje se do těchto callbacků umístit příkaz debugger, abyste mohli závislost interaktivně prozkoumat:

vue
<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue'

onRenderTracked((event) => {
  debugger
})

onRenderTriggered((event) => {
  debugger
})
</script>
js
export default {
  renderTracked(event) {
    debugger
  },
  renderTriggered(event) {
    debugger
  }
}

TIP

Debugging hooks komponent fungují pouze v režimu vývoje.

Objekty debug událostí mají následující typ:

ts
type DebuggerEvent = {
  effect: ReactiveEffect
  target: object
  type:
    | TrackOpTypes /* 'get' | 'has' | 'iterate' */
    | TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
  key: any
  newValue?: any
  oldValue?: any
  oldTarget?: Map<any, any> | Set<any>
}

Ladění computed proměnných

Computed proměnné můžeme ladit tak, že computed() předáme druhý objekt s možnostmi onTrack a onTrigger:

  • onTrack se zavolá, když je reaktivní vlastnost nebo ref sledován jako závislost.
  • onTrigger se zavolá, když je změnou závislosti spuštěn callback watcheru.

Oba callbacky obdrží debug události ve stejném formátu jako debug události komponent:

js
const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // spuštěno, když je count.value sledováno jako závislost
    debugger
  },
  onTrigger(e) {
    // spuštěno, když je count.value změněno
    debugger
  }
})

// přístup k plusOne, mělo by spustit onTrack
console.log(plusOne.value)

// změna count.value, mělo by spustit onTrigger
count.value++

TIP

Možnosti onTrack a onTrigger pro computed proměnné fungují pouze v režimu vývoje.

Ladění watcherů

Podobně jako computed(), watchery také podporují možnosti onTrack a onTrigger:

js
watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

watchEffect(callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

TIP

Možnosti onTrack a onTrigger watcheru fungují pouze v režimu vývoje.

Integrace s externími systémy pro správu stavu

Reaktivní systém Vue funguje tak, že převádí běžné JavaScriptové objekty na hluboce reaktivní proxy. Hluboká konverze může být zbytečná a někdy i nežádoucí při integraci s externími systémy pro správu stavu (např. pokud i externí řešení používá Proxies).

Obecná myšlenka integrace reaktivního systému Vue s externím řešením pro správu stavu je uchovávat externí stav v shallowRef. „Mělký“ ref je reaktivní pouze tehdy, když se přistupuje k jeho vlastnosti .value - vnitřní hodnota zůstává nedotčena. Při změně externího stavu nahraďte hodnotu ref, aby se spustily aktualizace.

Neměnná data

Pokud implementujete funkci undo / redo, pravděpodobně chcete po každé úpravě uživatele vytvořit snímek stavu aplikace. Nicméně pokud je strom stavu rozsáhlý, měnitelný reaktivní systém Vue pro to není nejvhodnější, protože serializace celého stavového objektu při každé aktualizaci může být drahá z hlediska výkonu a paměťových nákladů.

Neměnné (immutable) datové struktury řeší tento problém tím, že stavové objekty nikdy nemění - místo toho vytvářejí nové objekty, které s těmi starými sdílejí stejné, nezměněné části. Existuje různé způsoby použití neměnných dat v JavaScriptu, ale s Vue doporučujeme použít Immer, protože vám umožňuje používat neměnná data a zachovává ergonomičtější, měnitelnou syntaxi.

Immer můžeme integrovat s Vue pomocí jednoduché composable:

js
import { produce } from 'immer'
import { shallowRef } from 'vue'

export function useImmer(baseState) {
  const state = shallowRef(baseState)
  const update = (updater) => {
    state.value = produce(state.value, updater)
  }

  return [state, update]
}

Vyzkoušejte si to

Stavové automaty

Stavový automat (State machine) je model pro popis všech možných stavů, ve kterých se aplikace může nacházet, a všech možných způsobů, jak může přecházet z jednoho stavu do druhého. Ačkoli může být zbytečný pro jednoduché komponenty, může pomoci vytvořit robustnější a snadno spravovatelné složité stavové toky (state flows).

Jednou z nejpopulárnějších implementací stavových automatů v JavaScriptu je XState. Zde je composable, která s ním integruje:

js
import { createMachine, interpret } from 'xstate'
import { shallowRef } from 'vue'

export function useMachine(options) {
  const machine = createMachine(options)
  const state = shallowRef(machine.initialState)
  const service = interpret(machine)
    .onTransition((newState) => (state.value = newState))
    .start()
  const send = (event) => service.send(event)

  return [state, send]
}

Vyzkoušejte si to

RxJS

RxJS je knihovna pro práci s asynchronními event streamy. Knihovna VueUse poskytuje add-on @vueuse/rxjs na propojení RxJS proudů s reaktivním systémem Vue.

Propojení se signály

Celá řada dalších frameworků zavedla reaktivní prvky podobné refs z Composition API Vue pod termínem „signály“:

V jádru jsou signály stejným druhem prosté reaktivity (reactivity primitive) jako Vue refs. Jedná se o kontejner hodnot, který poskytuje sledování závislostí při přístupu a spouštění vedlejších efektů při změně. Toto paradigma založené na prosté reaktivitě není ve světě frontendu nijak zvlášť nový koncept: sahá až do implementací jako Knockout observables a Meteor Tracker z doby před více než deseti lety. Vue Options API a knihovna pro správu stavu Reactu MobX jsou také založeny na stejných principech, ale skrývají prostou reaktivitu za vlastnosti objektů.

I když to není nutná vlastnost, aby se něco kvalifikovalo jako signály, dnes se tento koncept často diskutuje ve spojitosti s modelem vykreslování, kde aktualizace probíhají prostřednictvím jemně granulovaných odběrů (fine-grained subscriptions). Vue v současné době díky použití virtuálního DOM pro dosažení podobných optimalizací spoléhá na kompilátory. Zkoumáme však také novou kompilační strategii inspirovanou Solid (režim Vapor), která na virtuální DOM nespoléhá a více využívá vestavěný reaktivní systém Vue.

Kompromisy návrhu API

Návrh signálů v Preactu a Qwik je velmi podobný Vue shallowRef: všechny tři poskytují měnitelné rozhraní prostřednictvím vlastnosti .value. Zaměříme se na diskuzi o signálech v Solidu a Angularu.

Solid Signály

Návrh API createSignal() v Solidu zdůrazňuje oddělení čtení a zápisu. Signály jsou přístupné jako getter pouze pro čtení a samostatný setter:

js
const [count, setCount] = createSignal(0)

count() // přístup k hodnotě
setCount(1) // aktualizace hodnoty

Všimněte si, jak může být signál count předán bez setteru. Tím se zajistí, že stav nemůže být nikdy měněn, pokud není setter explicitně vystaven. Zda tato záruka bezpečnosti ospravedlňuje více složitou syntaxi, může být závislé na požadavcích projektu a osobních preferencích - ale pokud tento styl API preferujete, můžete ho ve Vue snadno replikovat:

js
import { shallowRef, triggerRef } from 'vue'

export function createSignal(value, options) {
  const r = shallowRef(value)
  const get = () => r.value
  const set = (v) => {
    r.value = typeof v === 'function' ? v(r.value) : v
    if (options?.equals === false) triggerRef(r)
  }
  return [get, set]
}

Vyzkoušejte si to

Angular Signály

Angular prochází některými zásadními změnami, jak se vzdává konceptu dirty-checking a představuje vlastní implementaci prosté reaktivity. API pro signály v Angularu vypadá takto:

js
const count = signal(0)

count() // přístup k hodnotě
count.set(1) // nastavení nové hodnoty
count.update((v) => v + 1) // aktualizace na základě předchozí hodnoty

Toto API opět můžeme snadno replikovat ve Vue:

js
import { shallowRef } from 'vue'

export function signal(initialValue) {
  const r = shallowRef(initialValue)
  const s = () => r.value
  s.set = (value) => {
    r.value = value
  }
  s.update = (updater) => {
    r.value = updater(r.value)
  }
  return s
}

Vyzkoušejte si to

Ve srovnání s Vue refs poskytuje getter-based API styl Solidu a Angularu při použití ve Vue komponentách některé zajímavé kompromisy:

  • () je o něco kratší než .value, ale aktualizace hodnoty je rozsáhlejší.
  • Neexistuje ref-unwrapping: přístup k hodnotám vždy vyžaduje (). To zajišťuje všude konzistentní přístup k hodnotám. Také to znamená, že můžete předávat nezpracované signály jako vlastnosti (props) komponenty.

Zda se vám tyto API styly hodí, je do značné míry subjektivní. Naším cílem zde je demonstrovat podobnost a kompromisy mezi těmito různými návrhy API. Chceme také ukázat, že Vue je flexibilní: opravdu nejste uzamčeni do existujících API. Pokud je to nutné, můžete si vytvořit vlastní API pro reaktivitu, které lépe vyhovuje konkrétním potřebám.

Reaktivita podrobně has loaded