2019. május 29.

Így ne szívasd meg magad Vue.js-sel

10 perc olvasási idő

Így ne szívasd meg magad Vue.js-sel

Vannak hibák, amelyeket szinte minden fejlesztő elkövet, amikor először dolgozik Vue.js alapú projekten. A többségüket nem túl nagy mutatvány feltárni, gyakran elég csak felpattintani a konzolt a böngészőben, azonban létezik egy tipikus hiba, amely meglehetősen gonosz természeténél fogva jelentős károkat tud okozni az ember mentális épségében, ugyanis olyan triviálisnak tűnő műveletek kapcsán jelentkezik, mint például, amikor egy tömb egyik elemét meg szeretnénk változtatni a szokásos módon, ahogyan azt kezdő JavaScript fejlesztőként közvetlenül a "Hello World!" string konzolba történő kilogolását követően megtanultunk: this.items[0] = value.

Ránézésre minden rendben van ezzel a kódsorral és a konzol is üres, de valamiért mégsem történik semmi. Pedig ugye a reaktivitás lényege pont az lenne, hogy az alkalmazásod által megjelenített adatok bármilyen változás hatására frissülnek a képernyőn is anélkül, hogy neked ehhez bármit tenned kellene. A debuggerben ráadásul látod, hogy a változtatás megtörtént, de akkor miért nem jelenik meg?

Azért, mert bizonyos változásokról a Vue nem tud értesülni, ezért nem tudja, hogy újra kéne renderelni.

Ahhoz, hogy az ilyen hibákat el tudd kerülni, érdemes közelebbről megvizsgálni, hogy hogyan valósítja meg a reaktív működést a Vue.

Hogyan működik a Vue.js?

Példányosításkor a reaktív adatok (data, props stb.) getter-setter párokká alakulnak, valahogy így. A getterek és setterek lefutnak az adatok elérésekor, ezáltal lehetővé válik, hogy a Vue értesüljön az adatok változásáról. Ilyenkor triggerel egy rendert a virtuális DOM-ban a megváltozott adatok alapján, majd mindezt megjeleníti az alkalmazás konténerében.

Azonban – mivel ezek a getter/setter párok példányosításkor jönnek létre – a később hozzáadott további tulajdonságok változásairól a keretrendszer nem szerez tudomást.

Tömbök

A tömbökkel kapcsolatos problémák kicsit más okokra vezethetőek vissza, ugyanis ezek reaktivitását úgy oldották meg a fejlesztők, hogy becsomagolták az Array tagfüggvényeit (push, pop, splice, stb.). Amikor ezeken keresztül történik módosítás, minden megfelelően működik, azonban a [] operátor felüldefiniálására nincs lehetőség JavaScriptben, ebből kifolyólag a közvetlen módosításokról sajnos nem tudunk értesülni.

Mi a megoldás?

Sokan éreznek ilyen szituációkban késztetést a $forceUpdate() bevetésére, azonban ez a módszer – bár sok esetben működik – nem tekinthető megoldásnak. Ilyenkor ugyanis a Vue nem tudja, hogy miért volt szükség frissítésre, így azt sem tudja, hogy pontosan, mely komponenseket kell nulláról újrarenderelni. Ebből adódóan a komponens gyermekeit és azok gyermekeit (és azok gyermekeit stb.) is neked kell kézzel frissítgetned.

Ráadásul a $forceUpdate csak az újrarenderelést végzi el, ami nem feltétlenül elég, mivel nem kizárólag a megjelenítéskor okoz problémát, ha egy változásról nem értesül a keretrendszerünk. Előfordulhat, hogy például emiatt nem frissül a localStorage-ben tárolt state-ünk, vagy mondjuk nem küldjük vissza a szerverre a módosított profiladatokat. Ezeket a feladatokat is kézbe kell venned, az alkalmazásnak pedig innentől már nincs sok köze reaktivitáshoz.

Vue.set()

Az igazi megoldás a Vue.set() helper. Amikor nem obszerválható módosításra van szükség, ezen a függvényen keresztül van lehetőségünk azt elvégezni és egyúttal tudatni a keretrendszerrel, hogy történt valami, ami érdekes lehet a számára. Amennyiben objektumot kap értékként, létrehozza benne a szükséges getter/setter párokat, így az reaktívan fog viselkedni.

Példák

Összedobtam ezt a kis szösszenetet a tömbök manipulásával kapcsolatos problémák szemléltetésére:

See the Pen Vue.js arrays by Samu József (@sjozsef) on CodePen.


Ez a sor azért nem működik, mert a tömbök elemeinek közvetlen módosítása nem obszerválható:

this.items[0] = 'Title is not changed'

A következő megoldás azért jó, mert a $this.set() helper segítségével végeztem el a módosítást. Ez egyenértékű a Vue.set()-tel:

this.$set(this.items, 0, 'Title is changed')

Szintén jó megközelítés egy tömb method használata, mert az ezeken keresztül történt módosításokról tudomást szerez a Vue:

this.items.splice(0, 1, 'Title is changed')

A következő megoldás nem elegáns, de ebben az esetben működik, mert bár ugyanúgy nem értesül a változásról a keretrendszer, ahogy az első példában sem, kézzel lefuttatom a rendert. Ezt a megoldást azért implementáltam, hogy neked már ne kelljen. Ne próbáld ki otthon!

this.items[0]='May title is changed, but never do this.'
this.$forceUpdate()

Most nézzük meg, mi történik, ha a tömbben objektumok vannak? Ehhez is írtam példát:

See the Pen Vue.js Arrays with Objects by Samu József (@sjozsef) on CodePen.


Az első metódusom – talán meglepő módon – működik, mert bár első ránézésre úgy tűnhet, hogy közvetlenül módosítottam a tömbön, a valóságban az elemei változatlanok maradtak. Az objektumok JavaScriptben referenciaként adódnak át és tárolódnak le a tömbben, tehát abban lényegében csak memóriacímek vannak, amelyek az objektumok által lefoglalt memóriaterületekre mutatnak. Az objektum változtatásával a memóriacíme nem változik meg, maga az objektum pedig rendelkezik a Vue által léterhozott getterekkel és setterekkel:

this.todos[0].title='Changed title'

A következő sor azért nem működik, mert új objektumot hozok létre, és arra cserélem ki a tömb egyik elemét. Az új objektum új memóriacímet kap, ezért a tömbben változás történik, de erről a Vue nem értesül:

this.todos[0] = {
  title: 'Replaced title (not working)',
  description: 'Lorem ipsum'
}

Ráadásul itt egy olyan objektum jön létre, amelyek kulcsai nem kapják meg a getter/setter párjukat, ezért nem elég, hogy a változás nem jelenik meg, ezt követően már az egyébként helyes módosításom sem fog megfelelően működni. Ez egy eléggé aljas hiba, nem lennék a helyében annak a fejlesztőnek, akinek ki kell bogoznia.

Ha új objektumra akarom cserélni a tömböm egyik elemét, akkor – már biztos kitaláltad – a Vue.set() lesz a barátom:

this.$set(
  this.todos,
  0,
  {
    title: 'Replaced title',
    description: 'Lorem ipsum'
  }
)

Ilyenkor a Vue létrehozza a getter/setter párokat az újonnan beszúrt objektumhoz, tehát az előző példával szemben itt a továbbiakban is minden a várt módon fog működni.

A következő példák azt a problémakört igyekszenek szemléltetni, amikor objektumokhoz próbálunk meg hozzáadni új kulcsokat.

See the Pen Vue.js - adding object properties by Samu József (@sjozsef) on CodePen.


Az első metódus azért nem működik, mert a már létező objektumban közvetlenül hozok létre új kulcsot. Ehhez a kulcshoz nem tartozik getter/setter:

this.heading.excerpt = 'Lorem ipsum'

A következő megvalósítás rendben van, mert a Vue.set() az új kulcs hozzáadásakor a getter/setter párt is létrehozza. Azonban ha az előző sor már lefutott, az azt jelenti, hogy már létrejött az új kulcs getter/setter nélkül (annek ellenére, hogy nem látszik). A Vue.set() ilyenkor ezek meglétét nem vizsgálja, ezért az objektum sem most, sem a jövőben nem fog tudni reaktívan viselkedni.

Vue.set(
  this.heading,
  'excerpt',
  'Some lead text'
)

Azt is megtehetem büntetlenül, hogy az objektumot kicserélem egy újonnan létrehozott objektumra. Ez azért működik, mert itt a szülő objektum setterén keresztül érem el az objektumot, a Vue erről értesül, így létre tudja hozni a getter/setter párokat a hozzáadott objektumom kulcsaira.

this.heading = {
  title: 'Replaced title',
  excerpt: 'Replaced excerpt'
}

Ha ezt követően futtatod le az első példát, látható, hogy így már az is működik, mivel ebben az esetben nem új kulcsot hoz létre, hanem egy meglévőt módosít.

Konklúzió

A Vue.js nem véletlenül tartozik a top reaktív frameworkök közé, a változások követésével kapcsolatos megoldásai (is) zseniálisak. Ennek köszönhetjük, hogy a használata kényelmes, szinte észrevétlenül teszi a dolgát. Nem terhel minket feleslegesen, szebb és fókuszáltabb kódot tudunk írni. Azonban minden éremnek két oldala van. Mint mindig, érdemes tisztában lenni a háttérben történő folyamatokkal, mert vannak helyzetek, amikor az egyszerűség és a kényelem ellenünk fordul.

Azon fejlesztők közé tartozom, akik nagyon rosszul érzik magukat, amikor nem értik teljesen, hogy mi történik a színfalak mögött. Ez egy bizonytalan, kiszolgáltatott, kellemetlen helyzet. Minden frameworköt alaposan ismerni kell ahhoz, hogy ne lőjük vele lábon magunkat. Ebből a perspektívából a Vue.js nagy előnye a nála komplexebb keretrendszerekkel szemben, hogy az egyszerűségének köszönhetően a magabiztos használatához szükséges tudást jóval könnyebb felhalmozni.

Szóval remélem, hogy nem ijesztettelek el a használatától. Tényleg csak erre a két edge case-re kell odafigyelni, és akkor imádni fogod. Soha ne módosítsd közvetlenül a tömbök elemeit és ne adj hozzá közvetlenül új kulcsokat az objektumokhoz.

Köszönöm, hogy elolvastad.

Samu József

Webfejlesztő. Nyitott az újdonságokra - frontenden és backenden egyaránt. Igényli a valódi kihívásokat. Tudja, hogy csak akkor maradhat képben, ha minden nap képes fejlődni.

Samu József

Hozzászólások