Sügav sukeldumine JavaScripti WeakRefi ja FinalizationRegistrysse mälutõhusama vaatleja mustri loomiseks. Õppige, kuidas vältida suuri lekkeid.
JavaScripti WeakRefi vaatleja muster: mälutundlike sündmuste süsteemide loomine
Moodsa veebiarenduse maailmas on ühe lehe rakendused (SPA) muutunud dünaamiliste ja reageerivate kasutajakogemuste loomise standardiks. Need rakendused töötavad sageli pikema aja jooksul, hallates keerulist olekut ja käsitledes lugematuid kasutajate interaktsioone. Selle pikaealisusega kaasneb aga varjatud hind: suurenenud mälu lekkimise risk. Mälu leke, kus rakendus hoiab kinni mälu, mida ta enam ei vaja, võib aja jooksul jõudlust halvendada, põhjustades aeglustumist, brauseri krahhi ja halba kasutajakogemust. Üks levinumaid nende lekkide allikaid peitub põhialuses disainimustris: vaatleja mustris.
Vaatleja muster on sündmuspõhise arhitektuuri nurgakivi, mis võimaldab objektidel (vaatlejatel) tellida ja saada värskendusi keskselt objektilt (teema). See on elegantne, lihtne ja uskumatult kasulik. Kuid selle klassikalisel implementatsioonil on kriitiline viga: teema säilitab tugevaid viiteid oma vaatlejatele. Kui vaatlejat rakenduse ülejäänud osa enam ei vaja, kuid arendaja unustab selle teemast eksplitsiitselt lahti tellida, ei koguta seda kunagi prügi. See jääb mällu lõksu, kummitus, mis jälitab teie rakenduse jõudlust.
Siin pakub kaasaegne JavaScript oma ECMAScript 2021 (ES12) funktsioonidega võimsa lahenduse. Kasutades WeakRefi ja FinalizationRegistryt, saame luua mälutundliku vaatleja mustri, mis puhastab automaatselt enda järel, vältides neid levinud lekkeid. Käesolev artikkel on sügav sukeldumine sellesse täiustatud tehnikasse. Uurime problemet, mõistame tööriistu, ehitame nullist robustse implementatsiooni ja arutame, millal ja kus seda võimsat mustrit oma globaalsetes rakendustes rakendada tuleks.
Põhiprobleemi mõistmine: klassikaline vaatleja muster ja selle mälujälg
Enne lahenduse hindamist peame probleemi täielikult mõistma. Vaatleja muster, tuntud ka kui kirjastaja-tellija muster, on loodud komponentide lahti sidumiseks. Teema (või kirjastaja) säilitab loendi oma sõltuvatest objektidest, mida nimetatakse vaatlejateks (või tellijateks). Kui teema olek muutub, teavitab see automaatselt kõiki oma vaatlejaid, tavaliselt helistades neil kindlat meetodit, näiteks update().
Vaatame lihtsat, klassikalist implementatsiooni JavaScriptis.
Lihtne teema implementatsioon
Siin on põhiline teema klass. Sellel on meetodid vaatlejate tellimiseks, tellimuse tühistamiseks ja teavitamiseks.
class ClassicSubject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
console.log(`${observer.name} on tellinud.`);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
console.log(`${observer.name} on tellimuse tĂĽhistanud.`);
}
notify(data) {
console.log('Teema vaatlejaid...');
this.observers.forEach(observer => observer.update(data));
}
}
Ja siin on lihtne vaatleja klass, mis saab teema tellida.
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} sai andmeid: ${data}`);
}
}
Varjatud oht: pĂĽsivad viited
See implementatsioon töötab suurepäraselt, kuni me hoolikalt hallame oma vaatlejate elutsüklit. Probleem tekib siis, kui me seda ei tee. Kaaluge levinud stsenaariumi suures rakenduses: kaua kestev globaalne andmeladu (teema) ja ajutine UI komponent (vaatleja), mis kuvab osa neist andmetest.
Simuleerime seda stsenaariumi:
const dataStore = new ClassicSubject();
function manageUIComponent() {
let chartComponent = new Observer('ChartComponent');
dataStore.subscribe(chartComponent);
// Komponent teeb oma töö...
// NĂĽĂĽd kasutaja liigub mujale ja komponenti enam ei vajata.
// Arendaja võib unustada lisada puhastuskoodi:
// dataStore.unsubscribe(chartComponent);
chartComponent = null; // Vabastame oma viite komponendile.
}
manageUIComponent();
// Hiljem rakenduse elutsĂĽklis...
dataStore.notify('Uued andmed saadaval!');
Funktsioonis `manageUIComponent` loome `chartComponenti` ja tellime selle oma `dataStore`i. Hiljem seame `chartComponenti` nulliks, mis annab märku, et oleme sellega lõpetanud. Ootame, et JavaScripti prügikogur (GC) näeb, et selle objekti jaoks pole enam viiteid ja taastab selle mälu.
Kuid seal on veel üks viide! `dataStore.observers` massiiv hoiab endiselt otsest, tugevat viidet `chartComponent` objektile. Selle ühe püsiva viite tõttu ei saa prügikogur mälu taastada. `chartComponent` objekt ja kõik selle poolt hoitavad ressursid jäävad `dataStore`i kogu elueaks mällu. Kui see juhtub korduvalt – näiteks iga kord, kui kasutaja avab ja sulgeb modaalakna – kasvab rakenduse mälukasutus lõputult. See on klassikaline mäluleke.
Uus lootus: tutvustame WeakRefi ja FinalizationRegistryt
ECMAScript 2021 tutvustas kahte uut funktsiooni, mis on spetsiaalselt loodud selliste mäluhalduse väljakutsete lahendamiseks: `WeakRef` ja `FinalizationRegistry`. Need on täiustatud tööriistad ja neid tuleks kasutada ettevaatlikult, kuid meie vaatleja mustri probleemile on need täiuslik lahendus.
Mis on WeakRef?
`WeakRef` objekt hoiab nõrka viidet teisele objektile, mida nimetatakse selle sihtmärgiks. Peamine erinevus nõrga viite ja tavalise (tugeva) viite vahel on see: nõrk viide ei takista selle sihtmärgi objekti prügi kogumist.
Kui ainsad viited objektile on nõrgad viited, on JavaScripti mootoril vabadus objekti hävitada ja selle mälu taastada. See on täpselt see, mida me vajame oma vaatleja probleemi lahendamiseks.
`WeakRef`i kasutamiseks loote selle instansi, edastades sihtmärgiobjekti konstruktorile. Hiljem sihtmärgiobjekti juurde pääsemiseks kasutate `deref()` meetodit.
let targetObject = { id: 42 };
const weakRefToObject = new WeakRef(targetObject);
// Objekti juurde pääsemiseks:
const retrievedObject = weakRefToObject.deref();
if (retrievedObject) {
console.log(`Objekt on endiselt elus: ${retrievedObject.id}`); // Väljund: Objekt on endiselt elus: 42
} else {
console.log('Objekt on prĂĽgi kogutud.');
}
Kriitiline osa on see, et `deref()` võib tagastada `undefined`. See juhtub, kui `targetObject` on prügi kogutud, kuna sellele pole enam tugevaid viiteid. See käitumine on meie mälutundliku vaatleja mustri alus.
Mis on FinalizationRegistry?
Kuigi `WeakRef` võimaldab objekti koguda, ei anna see meile puhast viisi teada, millal see on kogutud. Võiksime perioodiliselt kontrollida `deref()` ja eemaldada `undefined` tulemused oma vaatlejate loendist, kuid see on ebaefektiivne. Siin tuleb `FinalizationRegistry`.
`FinalizationRegistry` võimaldab teil registreerida puhastusfunktsiooni, mida kutsutakse välja pärast registreeritud objekti prügi kogumist. See on järeltöötluse puhastusmehhanism.
Siin on, kuidas see töötab:
- Loote registri puhastuskõnega.
- Te registreerite objekti `register()` registris. Võite edastada ka `heldValue`, mis on andmete tükk, mida edastatakse teie kõnele, kui objekt on kogutud. See `heldValue` ei tohi olla otsene viide objektile endale, kuna see rikuks eesmärgi!
// 1. Loo registri puhastuskõnega
const registry = new FinalizationRegistry(heldValue => {
console.log(`Objekt on prĂĽgi kogutud. Puhastuse tunnus: ${heldValue}`);
});
(function() {
let objectToTrack = { name: 'Ajutised Andmed' };
let cleanupToken = 'temp-data-123';
// 2. Registreerige objekt ja edastage puhastuse tunnus
registry.register(objectToTrack, cleanupToken);
// objectToTrack läheb siin ulatusest välja
})();
// Mõne aja pärast pärast GC käivitamist logib konsool:
// "Objekt on prĂĽgi kogutud. Puhastuse tunnus: temp-data-123"
Olulised hoiatused ja parimad tavad
Enne implementatsiooni sukeldumist on kriitilise tähtsusega mõista nende tööriistade olemust. Prügikoguja käitumine sõltub suuresti implementatsioonist ja on mitte-deterministlik. See tähendab:
- Te ei saa ennustada, millal objekt kogutakse. See võib olla sekundeid, minuteid või isegi kauem pärast seda, kui see muutub kättesaamatuks.
- Te ei saa loota `FinalizationRegistry` kõnedele, et need toimuksid õigeaegselt või prognoositavalt. Need on puhastuseks, mitte kriitiliseks rakenduse loogikaks.
- `WeakRef`i ja `FinalizationRegistry` liigne kasutamine võib koodi raskemini mõistetavaks muuta. Eelistage alati lihtsamaid lahendusi (nagu eksplitsiitne `unsubscribe()` kõned), kui objektide elutsüklid on selged ja hallatavad.
Need funktsioonid sobivad kõige paremini olukordadesse, kus ühe objekti (vaatleja) elutsükkel on tõeliselt sõltumatu ja teadmata teisest objektist (teema).
`WeakRefObserver` mustri loomine: samm-sammult implementatsioon
Nüüd kombineerime `WeakRef`i ja `FinalizationRegistry`t, et luua mäluturvaline `WeakRefSubject` klass.
Samm 1: `WeakRefSubject` klassi struktuur
Meie uus klass salvestab vaatlejate `WeakRef`e otseviidete asemel. Sellel on ka `FinalizationRegistry` vaatlejate loendi automaatseks puhastamiseks.
class WeakRefSubject {
constructor() {
this.observers = new Set(); // Lihtsamaks eemaldamiseks kasutame Seti
// Lõpetaja kõne. See saab registreerimise ajal edastatud väärtuse.
// Meie puhul on väärtus ise WeakRef instans.
this.cleanupRegistry = new FinalizationRegistry(weakRefObserver => {
console.log('Lõpetaja: Vaatleja on prügi kogutud. Puhastan...');
this.observers.delete(weakRefObserver);
});
}
}
Kasutame vaatlejate loendi jaoks `Set`i massiivi asemel. See on sellepärast, et `Set`ist üksuse kustutamine on palju tõhusam (keskmiselt O(1) ajakompleksus) kui `Array` filtreerimine (O(n)), mis on meie puhastusloogikas kasulik.
Samm 2: `subscribe` meetod
`subscribe` meetod on koht, kus algab maagia. Kui vaatleja tellib, siis:
- Loome `WeakRef`i, mis osutab vaatlejale.
- Lisame selle `WeakRef`i meie `observers` setti.
- Registreerime algse vaatlejate objekti meie `FinalizationRegistry`ga, kasutades äsja loodud `WeakRef`i kui `heldValue`t.
// `WeakRefSubject` klassi sees...
subscribe(observer) {
// Kontrollige, kas selle viitega vaatleja juba eksisteerib
for (const ref of this.observers) {
if (ref.deref() === observer) {
console.warn('Vaatleja on juba tellinud.');
return;
}
}
const weakRefObserver = new WeakRef(observer);
this.observers.add(weakRefObserver);
// Registreerige algne vaatlejate objekt. Kui see on kogutud,
// lõpetaja kutsutakse välja `weakRefObserver`iga argumendina.
this.cleanupRegistry.register(observer, weakRefObserver);
console.log('Vaatleja on tellinud.');
}
See seadistus loob nutika tsükli: teema hoiab vaatlejast nõrka viidet. Register hoiab vaatlejast tugevat viidet (seespidiselt), kuni see on prügi kogutud. Kui see on kogutud, käivitatakse registri kõne, mida saame kasutada meie `observers` seti puhastamiseks.
Samm 3: `unsubscribe` meetod
Isegi automaatse puhastusega peaksime ikkagi pakkuma manuaalse `unsubscribe` meetodi juhtumite jaoks, kus deterministlik eemaldamine on vajalik. See meetod peab leidma meie setist õige `WeakRef`i, dereferentseerides igaüks ja võrreldes seda vaatlejaga, mida soovime eemaldada.
// `WeakRefSubject` klassi sees...
unsubscribe(observer) {
let refToRemove = null;
for (const weakRef of this.observers) {
if (weakRef.deref() === observer) {
refToRemove = weakRef;
break;
}
}
if (refToRemove) {
this.observers.delete(refToRemove);
// TÄHTIS: Peame ka lõpetajast lahti registreerima
// et vältida kõne hilisemat tarbetut käivitumist.
this.cleanupRegistry.unregister(observer);
console.log('Vaatleja on manuaalselt tellimuse tĂĽhistanud.');
}
}
Samm 4: `notify` meetod
`notify` meetod itereerib üle meie `WeakRef`ide seti. Igaühe jaoks proovib see seda `deref()`ida, et saada tegelik vaatlejate objekt. Kui `deref()` õnnestub, tähendab see, et vaatleja on veel elus ja me saame kutsuda tema `update` meetodit. Kui see tagastab `undefined`, on vaatleja kogutud ja me võime seda ignoreerida. `FinalizationRegistry` eemaldab lõpuks selle `WeakRef`i setist.
// `WeakRefSubject` klassi sees...
notify(data) {
console.log('Teavitab vaatlejaid...');
for (const weakRefObserver of this.observers) {
const observer = weakRefObserver.deref();
if (observer) {
// Vaatleja on veel elus
observer.update(data);
} else {
// Vaatleja on prĂĽgi kogutud.
// FinalizationRegistry hoolitseb selle weakRefi eemaldamisest setist.
console.log('Teavituse ajal leiti surnud vaatleja viide.');
}
}
}
Kõik koos: praktiline näide
Vaatame uuesti meie UI komponendi stsenaariumi, kuid seekord kasutades meie uut `WeakRefSubject`i. Lihtsuse huvides kasutame sama `Observer` klassi nagu varem.
// Sama lihtne Observer klass
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} sai andmeid: ${data}`);
}
}
NĂĽĂĽd loome globaalse andmeteenuse ja simuleerime ajutist UI vidinat.
const globalDataService = new WeakRefSubject();
function createAndDestroyWidget() {
console.log('--- Uue vidina loomine ja tellimine ---');
let chartWidget = new Observer('RealTimeChartWidget');
globalDataService.subscribe(chartWidget);
// Vidin on nĂĽĂĽd aktiivne ja saab teateid
globalDataService.notify({ price: 100 });
console.log('--- Vidina hävitamine (meie viite vabastamine) ---');
// Oleme vidinaga lõpetanud. Seame oma viite nulliks.
// Me EI pea kutsuma unsubscribe().
chartWidget = null;
}
createAndDestroyWidget();
console.log('--- Pärast vidina hävitamist, enne prügikogumist ---');
globalDataService.notify({ price: 105 });
Pärast `createAndDestroyWidget()` käivitamist viitab `chartWidget` objekt nüüd ainult meie `globalDataService` sees olevale `WeakRef`ile. Kuna tegemist on nõrga viitega, on objekt nüüd prügi kogumiseks kõlblik.
Kui prügikogur lõpuks töötab (mida me ei saa ennustada), juhtub kaks asja:
- `chartWidget` objekt eemaldatakse mälust.
- Meie `FinalizationRegistry` kõne käivitatakse, mis eemaldab surnud `WeakRef`i `globalDataService.observers` setist.
Kui kutsume pärast prügikoguri käivitamist uuesti `notify`d, tagastab `deref()` kõne `undefined`, surnud vaatlejat ignoreeritakse ja rakendus jätkab tõhusalt töötamist ilma mäluleketeta. Oleme edukalt lahti sidunud vaatleja elutsükli teema omast.
Millal kasutada (ja millal vältida) `WeakRefObserver` mustrit
See muster on võimas, kuid see pole imerohi. See toob kaasa keerukust ja tugineb mitte-deterministlikule käitumisele. On oluline teada, millal see on õige tööriist.
Ideaaljuhtumid
- Pikalt kestvad teemad ja lühiajalised vaatlejad: See on kanooniline kasutusjuhtum. Globaalne teenus, andmeladu või vahemälu (teema), mis eksisteerib kogu rakenduse elutsükli jooksul, samal ajal kui arvukad UI komponendid, ajutised töölised või pistikprogrammid (vaatlejad) luuakse ja hävitatakse sageli.
- Vahemälu mehhanismid: Kujutage ette vahemälu, mis kaardistab keeruka objekti mõne arvutatud tulemuse järgi. Saate kasutada `WeakRef`i algobjekti jaoks. Kui algne objekt on rakenduse ülejäänud osast prügi kogutud, saab `FinalizationRegistry` automaatselt puhastada vastava kirje teie vahemälust, vältides mälu paisumist.
- Pistikprogrammide ja laienduste arhitektuurid: Kui loote põhisüsteemi, mis võimaldab kolmandate osapoolte moodulitel sündmusi tellida, lisab `WeakRefObserver` vastupidavuse kihi. See takistab halvasti kirjutatud pistikprogrammi, mis unustab tellimuse tühistada, põhjustada mälu leket teie põh rakenduses.
- Andmete kaardistamine DOM-elementidele: Deklaratiivsete raamistike puudumisel võite soovida seostada mõned andmed DOM-elemendiga. Kui salvestate selle kaardil DOM-elemendi võtmena, võite tekitada mälulekke, kui element eemaldatakse DOM-ist, kuid on ikka veel teie kaardil. `WeakMap` on siin parem valik, kuid põhimõte on sama: andmete elutsükkel peaks olema seotud elemendi elutsükliga, mitte vastupidi.
Millal jääda klassikalise vaatleja juurde
- Tihedalt seotud elutsüklid: Kui teema ja selle vaatlejad luuakse ja hävitatakse alati koos või sama ulatusega, on `WeakRef`i lisakoormus ja keerukus tarbetud. Lihtne, eksplitsiitne `unsubscribe()` kõne on loetavam ja prognoositavam.
- Jõudluskriitilised kuumad teed: `deref()` meetodil on väike, kuid mitte-null jõudluskulu. Kui te teavitate tuhandeid vaatlejaid sadu kordi sekundis (nt mängu tsüklis või kõrgsageduslikul andmevisualisatsioonil), on klassikaline implementatsioon otseviidetega kiirem.
- Lihtsad rakendused ja skriptid: Väiksemate rakenduste või skriptide puhul, kus rakenduse eluiga on lühike ja mälu haldamine pole oluline mure, on klassikaline muster lihtsam implementeerida ja mõista. Ärge lisage keerukust, kus seda pole vaja.
- Kui on vaja deterministlikku puhastust: Kui peate tegema toimingu täpsel hetkel, kui vaatleja lahti ühendatakse (nt loenduri värskendamine, spetsiifilise riistvararesursi vabastamine), peate kasutama manuaalset `unsubscribe()` meetodit. `FinalizationRegistry` mitte-deterministlik käitumine muudab selle sobimatuks loogikale, mis peab prognoositavalt täituma.
Laiemad mõjud tarkvara arhitektuurile
Nõrkade viidete tutvustamine kõrgetasemelises keeles nagu JavaScript annab platvormi küpsusest märku. See võimaldab arendajatel luua keerukamaid ja vastupidavamaid süsteeme, eriti kaua kestvate rakenduste jaoks. See muster soodustab arhitektuurilise mõtteviisi muutust:
- Tõeline lahti sidumine: See võimaldab sidumise taset, mis läheb kaugemale ainult liidesest. Saame nüüd siduda komponentide endiste elutsüklite lahti. Teema ei pea enam teadma midagi selle kohta, millal selle vaatlejad luuakse või hävitatakse.
- Vastupidavus disaini järgi: See aitab luua süsteeme, mis on vastupidavamad programmeerimisveale. Unustatud `unsubscribe()` kõne on tavaline viga, mida on raske jälgida. See muster leevendab kogu seda viga klassi.
- Raamistiku ja raamatukogu autorite võimaldamine: Neile, kes loovad teistele arendajatele raamistikke, raamatukogusid või platvorme, on need tööriistad hindamatud. Need võimaldavad luua robustseid API-sid, mis on vähem vastuvõtlikud raamatukogu tarbijate poolt väärkasutusele, mis viib üldiselt stabiilsemate rakendusteni.
Kokkuvõte: Võimas tööriist moodsa JavaScripti arendajale
Klassikaline vaatleja muster on tarkvara disaini põhiline ehitusplokk, kuid selle sõltuvus tugevatest viidetest on pikka aega olnud JavaScripti rakendustes peenete ja frustreerivate mälulekete allikas. ES2021-s `WeakRef`i ja `FinalizationRegistry` tutvustamisega on meil nüüd vahendid selle piirangu ületamiseks.
Oleme rännanud alates püsivate viidete põhiprobleemi mõistmisest kuni täieliku, mälutundliku `WeakRefSubject`i loomiseni algusest peale. Oleme näinud, kuidas `WeakRef` võimaldab objekte prügi koguda isegi siis, kui neid "vaadeldakse", ja kuidas `FinalizationRegistry` pakub automatiseeritud puhastusmehhanismi meie vaatlejate loendi laitmatuks hoidmiseks.
Kuid suure võimuga kaasneb suur vastutus. Need on täiustatud funktsioonid, mille mitte-deterministlik käitumine nõuab hoolikat kaalumist. Need ei ole hea rakenduse disaini ja hoolika elutsükli haldamise asendus. Kuid kui neid rakendatakse õigetele probleemidele – nagu pikaajaliste teenuste ja efemeersete komponentide vahelise suhtluse haldamine – on WeakRef Observer muster erakordselt võimas tehnika. Omandades selle, saate kirjutada robustsemaid, tõhusamaid ja skaleeritavamaid JavaScripti rakendusi, mis on valmis vastama tänapäevase, dünaamilise veebi nõudmistele.