Kas iš tikrųjų yra reaktyvumas ir kodėl tai svarbu
Jei kada nors dirbai su vanilla JavaScript ir rankiniu būdu atnaujinai DOM elementus kiekvieną kartą, kai pasikeitė duomenys, tikriausiai žinai, kaip greitai tas kodas tampa netvarkingu košmaru. Reaktyvumas iš esmės sprendžia šią problemą – vietoj to, kad tu pats sektum, kas pasikeitė ir ką reikia atnaujinti, sistema tai daro už tave.
Vue.js reaktyvumo sistema veikia pagal labai paprastą, bet galingą principą: kai duomenys pasikeičia, viskas, kas nuo jų priklauso, automatiškai atsinaujina. Tai skamba paprastai, bet po tuo slypi nemažai inžinerinių sprendimų, kurie lemia, kaip efektyviai ir nuspėjamai viskas veikia.
Vue 2 naudojo Object.defineProperty() metodą, kad „perima” objektų savybių skaitymo ir rašymo operacijas. Vue 3 perėjo prie Proxy API, kuris suteikia daug daugiau galimybių ir pašalina nemažai senojo metodo apribojimų. Pavyzdžiui, Vue 2 negalėjo reaktyviai sekti naujų savybių pridėjimo prie objekto arba masyvų indeksų keitimo – tai buvo žinomi „gotcha” momentai, kurie gali sugadinti ne vieną valandą.
Praktiškai tai reiškia, kad Vue 3 su Proxy pagrindu veikiančia sistema gali sekti bet kokius objekto pakeitimus, įskaitant naujų savybių pridėjimą, ištrynimą ir net masyvų mutacijas per indeksus. Tai didelis žingsnis į priekį.
ref() ir reactive() – kuo jie skiriasi ir kada kurį naudoti
Vienas dažniausių klausimų, su kuriais susiduria žmonės pradėję dirbti su Vue 3 Composition API, yra: „Kada naudoti ref(), o kada reactive()?” Atsakymas nėra visiškai vienareikšmis, bet yra keletas gairių, kurios padeda apsispręsti.
ref() sukuria reaktyvią nuorodą į bet kokią reikšmę – primityvus tipus (skaičius, eilutės, boolean), objektus, masyvus. Svarbiausia žinoti, kad norint pasiekti ar pakeisti reikšmę JavaScript kode, reikia naudoti .value savybę:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
Šablonuose (<template>) Vue automatiškai „išpakuoja” ref reikšmes, tad ten .value rašyti nereikia – tiesiog naudoji {{ count }}.
reactive() veikia kitaip – jis paverčia visą objektą reaktyviu ir nereikia .value:
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: 'Jonas'
})
state.count++ // tiesiogiai, be .value
Bet čia yra subtilybė, kuri gali sukelti problemų: reactive() objekto negalima „destrukturizuoti” išlaikant reaktyvumą. Jei parašysi const { count } = state, gausi paprastą, nereaktyvų skaičių. Tam yra toRefs() funkcija:
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0 })
const { count } = toRefs(state) // dabar count yra reaktyvi ref
Praktinė rekomendacija: daugelis patyrusių Vue kūrėjų tiesiog visur naudoja ref() – taip kodas yra nuoseklesnis ir mažiau klaidų. reactive() praverčia, kai turi susijusių duomenų grupę ir nori juos laikyti kartu kaip loginį vienetą.
Computed savybės – ne tik patogumas, bet ir našumas
Computed savybės yra vienas tų dalykų, kuriuos pradžioje gali atrodyti kaip paprastas sintaksinis cukrus, bet iš tikrųjų jos daro kažką svarbaus – kešuoja rezultatus.
Tarkime, turi sąrašą produktų ir nori rodyti tik tuos, kurie kainuoja mažiau nei 50 eurų. Galėtum tai daryti tiesiog šablone su metodu:
// Metodas - skaičiuojamas kiekvieną kartą, kai komponentas atsinaujina
methods: {
filteredProducts() {
return this.products.filter(p => p.price < 50)
}
}
Arba su computed:
import { ref, computed } from 'vue'
const products = ref([...])
const filteredProducts = computed(() => {
return products.value.filter(p => p.price < 50)
})
Skirtumas esminis: computed savybė perskaičiuojama tik tada, kai pasikeičia jos priklausomybės (šiuo atveju – products). Metodas vykdomas kiekvieną kartą, kai komponentas perpiešiamas, nepriklausomai nuo to, ar duomenys pasikeitė. Jei turi sunkią filtravimo ar skaičiavimo operaciją ir komponentas dažnai atsinaujina dėl kitų priežasčių, skirtumas gali būti labai juntamas.
Dar vienas dalykas, kurį mažiau žmonių žino – computed savybės gali būti ne tik skaitomos, bet ir rašomos:
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
const parts = newValue.split(' ')
firstName.value = parts[0]
lastName.value = parts[1] || ''
}
})
Tai praverčia, kai nori dvikrypčio duomenų srauto su transformacija.
watch ir watchEffect – reaktyvus šalutinių efektų valdymas
Kartais reaktyvaus atnaujinimo šablone nepakanka – reikia reaguoti į duomenų pasikeitimus ir atlikti kažką kita: siųsti HTTP užklausą, išsaugoti į localStorage, paleisti animaciją. Tam skirtos watch ir watchEffect funkcijos.
watch yra tikslingas – tu nurodysi, ką stebėti, ir reakcija įvyksta tik tada, kai tas konkretus dalykas pasikeičia:
import { ref, watch } from 'vue'
const searchQuery = ref('')
watch(searchQuery, (newValue, oldValue) => {
console.log(`Ieškoma: ${newValue}, buvo: ${oldValue}`)
fetchResults(newValue)
})
Galima stebėti kelis šaltinius vienu metu:
watch([firstName, lastName], ([newFirst, newLast]) => {
console.log(`Vardas pasikeitė: ${newFirst} ${newLast}`)
})
watchEffect veikia kitaip – jis pats automatiškai nustato, ką stebėti, pagal tai, ką naudoji viduje:
import { watchEffect } from 'vue'
watchEffect(() => {
// Vue automatiškai seka, kad čia naudojami searchQuery ir currentPage
fetchResults(searchQuery.value, currentPage.value)
})
Tai patogiau, kai turi daug priklausomybių ir nenori jų visų išvardinti rankiniu būdu. Bet yra vienas niuansas – watchEffect paleidžiamas iš karto (pirmą kartą be jokio pakeitimo), o watch pagal nutylėjimą laukia, kol kažkas pasikeičia. Jei nori, kad watch taip pat paleistų iš karto, naudok { immediate: true } opciją.
Svarbu nepamirši ir valymo – jei stebėtojas paleidžia asinchroninę operaciją, gali kilti problemų, jei komponentas sunaikinamas prieš jai pasibaigiant. watchEffect suteikia onCleanup funkciją šiai situacijai tvarkyti:
watchEffect((onCleanup) => {
const controller = new AbortController()
fetch(url, { signal: controller.signal })
onCleanup(() => {
controller.abort() // atšaukiame užklausą, jei komponentas sunaikinamas
})
})
Reaktyvumas tarp komponentų – Provide/Inject ir Pinia
Reaktyvumas vieno komponento viduje yra vienas dalykas, bet realios aplikacijos reikalauja dalintis duomenimis tarp komponentų. Čia atsiranda keletas skirtingų sprendimų, ir kiekvienas tinkamas skirtingoms situacijoms.
Paprasčiausias atvejis – tėvinis ir vaikinis komponentai. Duomenys perduodami per props žemyn, o įvykiai keliami aukštyn. Tai Vue pagrindinis modelis ir jis puikiai veikia, kol hierarchija nėra per gili.
Kai komponentų medis tampa gilesnis ir duomenis reikia perduoti per kelis lygius, atsiranda "prop drilling" problema – kiekvienas tarpinis komponentas turi priimti ir perduoti props, kurių jam pačiam nereikia. Tam Vue siūlo provide ir inject:
// Tėviniame komponente
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme) // perduodame reaktyvią reikšmę
// Bet kuriame vaikiniame komponente, nepriklausomai nuo gylio
import { inject } from 'vue'
const theme = inject('theme') // gauna reaktyvią reikšmę
Svarbu perduoti pačią ref reikšmę (ne ref.value), kad reaktyvumas išliktų.
Kai aplikacija auga ir reikia globalios būsenos valdymo, čia įeina Pinia – oficialiai rekomenduojama Vue būsenos valdymo biblioteka (Vue 3 eroje ji pakeitė Vuex). Pinia store'ai patys savaime yra reaktyvūs:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const isLoggedIn = computed(() => user.value !== null)
function login(userData) {
user.value = userData
}
function logout() {
user.value = null
}
return { user, isLoggedIn, login, logout }
})
Pinia store'ą naudoji komponente tiesiog kaip įprastą Composition API kodą – visas reaktyvumas veikia automatiškai.
Dažniausios reaktyvumo klaidos ir kaip jų išvengti
Net ir suprantant teoriją, praktikoje reaktyvumo klaidos pasitaiko dažnai. Štai keletas labiausiai paplitusių situacijų, kurios gali sukelti galvos skausmą.
Reaktyvumo praradimas su destrukturizacija. Jau minėjome reactive() atvejį, bet ta pati problema gali kilti ir su Pinia store'ais:
// Blogai - praranda reaktyvumą
const { user, isLoggedIn } = useUserStore()
// Gerai - išlaiko reaktyvumą
const store = useUserStore()
// arba
const { user, isLoggedIn } = storeToRefs(useUserStore())
Tiesioginis masyvo mutavimas nepalaikomomis operacijomis. Vue 3 su Proxy šią problemą iš esmės išsprendė, bet vis tiek geriau naudoti reaktyvias operacijas:
const items = ref([1, 2, 3])
// Tai veikia Vue 3, bet geroji praktika:
items.value = [...items.value, 4] // naujas masyvas
// arba
items.value.push(4) // mutacija, bet Vue 3 ją seka
Asinchroninis reaktyvumas. Jei keiti reaktyvius duomenis asinchroninėje funkcijoje, viskas veikia gerai. Bet jei nori palaukti, kol DOM atsinaujins po duomenų pakeitimo, reikia nextTick():
import { nextTick } from 'vue'
async function updateAndFocus() {
items.value.push(newItem)
await nextTick() // laukiame, kol DOM atsinaujins
inputRef.value.focus() // dabar galime dirbti su atnaujintu DOM
}
Stebėtojų atminties nutekėjimas. Stebėtojai, sukurti setup() viduje, automatiškai sunaikinami, kai komponentas sunaikinamas. Bet jei sukuri stebėtoją už komponento ribų (pvz., globaliame faile), jį reikia sunaikinti rankiniu būdu:
const stop = watch(someRef, () => { ... })
// Kai nebereikia:
stop()
Šablonų kompiliavimas ir reaktyvumo optimizavimas
Vue šablonai nėra tiesiog HTML – jie kompiliuojami į JavaScript render funkcijas. Šis kompiliavimo žingsnis leidžia Vue atlikti optimizacijas, kurių rankiniu būdu parašytas kodas negalėtų.
Vue 3 įvedė "Block tree" optimizaciją ir statinių mazgų kėlimą (static hoisting). Tai reiškia, kad statinis turinys (kuris niekada nesikeičia) kompiliuojamas vieną kartą ir pakartotinai naudojamas, o dinaminis turinys žymimas specialiai, kad atnaujinimo metu reikėtų patikrinti tik jį.
Praktiškai tau dažniausiai nereikia galvoti apie šias optimizacijas – Vue jas atlieka automatiškai. Bet yra keletas dalykų, kuriuos gali daryti pats, kad pagerintum našumą:
Naudok v-once turiniui, kuris tikrai niekada nesikeičia po pirmojo renderio:
<h1 v-once>{{ staticTitle }}</h1>
Naudok v-memo sąrašams su sudėtingais elementais, kai nori kontroliuoti, kada elementas perpiešiamas:
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
<!-- perpiešiamas tik kai pasikeičia id arba selected -->
</div>
Naudok shallowRef() ir shallowReactive() dideliems objektams, kai reaktyvumo reikia tik pirmame lygyje:
// Vue neseka giliai įdėtų savybių - geriau našumui su dideliais duomenimis
const bigData = shallowRef({ nested: { deeply: { value: 42 } } })
Reaktyvumas kaip mąstymo būdas, o ne tik įrankis
Dirbant su Vue ilgiau, pradedi suprasti, kad reaktyvumas nėra tik techninė detalė – tai kitoks būdas galvoti apie aplikacijos būseną ir duomenų srautą. Vietoj to, kad galvotum "ką reikia atnaujinti, kai vartotojas paspaudžia šį mygtuką", pradedi galvoti "kaip atrodo mano aplikacijos būsena ir kaip UI turėtų atspindėti tą būseną".
Šis mentalinis modelis iš pradžių gali atrodyti keistai, ypač jei esi įpratęs prie imperatyvaus programavimo. Bet kai jis "paspaudžia", kodas tampa daug aiškesnis ir lengviau prižiūrimas. Vietoj ilgų funkcijų, kurios rankiniu būdu atnaujina dešimtis DOM elementų, turi nedidelę būsenos logiką ir šabloną, kuris automatiškai atspindi tą būseną.
Keletas patarimų, kurie padės greičiau internalizuoti šį mąstymo būdą: pirma, visada galvok apie "single source of truth" – kiekvienas duomenų gabalas turėtų gyventi vienoje vietoje, o visi kiti dalykai turėtų būti išvedami iš jo. Antra, computed savybės yra tavo draugai – jei kažkas gali būti apskaičiuota iš esamos būsenos, tai turėtų būti computed, o ne atskira būsena. Trečia, stebėtojai (watch) turėtų būti naudojami tik šalutiniams efektams – jei tiesiog nori transformuoti duomenis rodymui, computed yra tinkamesnis pasirinkimas.
Vue reaktyvumo sistema yra viena iš tų technologijų, kur kuo daugiau laiko praleidi su ja, tuo labiau pradedi vertinti jos dizaino sprendimus. Nuo Vue 2 Object.defineProperty() iki Vue 3 Proxy pagrindu veikiančios sistemos – kiekvienas žingsnis buvo skirtas pašalinti apribojimus ir padaryti kūrėjų gyvenimą lengvesnį. Ir nors visada bus kraštutinių atvejų ir subtilybių, kurių reikia mokytis, pagrindinis pažadas išlieka tas pats: rūpinkis savo duomenimis, o Vue pasirūpins likusiu.






