Front-End-Eintrag (vue) zum Kompetenzkurs: Der Einstieg zum Erlernen von JavaScript bietet keine Speicherverwaltungsvorgänge. Stattdessen wird der Speicher von der JavaScript-VM durch einen Speicherrückgewinnungsprozess namens Garbage Collection verwaltet.
Woher wissen wir, dass die Speicherbereinigung funktioniert, da wir sie nicht erzwingen können? Wie viel wissen wir darüber?
Die Skriptausführung wird während dieses Vorgangs angehalten
Es gibt Speicher für unzugängliche Ressourcen frei
es ist ungewiss
Es überprüft nicht den gesamten Speicher auf einmal, sondern läuft in mehreren Zyklen
Es ist unvorhersehbar, aber es wird funktionieren, wenn es nötig ist
Bedeutet das, dass Sie sich über Probleme bei der Ressourcen- und Speicherzuweisung keine Sorgen machen müssen? Wenn wir nicht aufpassen, kann es zu Speicherlecks kommen.
Ein Speicherverlust ist ein Block zugewiesenen Speichers, den die Software nicht zurückgewinnen kann.
Javascript bietet einen Garbage Collector, aber das bedeutet nicht, dass wir Speicherlecks vermeiden können. Um für die Garbage Collection in Frage zu kommen, darf das Objekt nicht an anderer Stelle referenziert werden. Wenn Sie Verweise auf ungenutzte Ressourcen enthalten, verhindert dies, dass diese Ressourcen zurückgefordert werden. Dies wird als unbewusste Gedächtniserhaltung bezeichnet.
Ein Speicherverlust kann dazu führen, dass der Garbage Collector häufiger ausgeführt wird. Da dieser Vorgang die Ausführung des Skripts verhindert, kann es sein, dass unser Programm einfriert. Wenn eine solche Verzögerung auftritt, werden wählerische Benutzer definitiv bemerken, dass das Produkt für längere Zeit offline sein wird, wenn sie damit nicht zufrieden sind. Noch schlimmer ist, dass die gesamte Anwendung abstürzen kann, was gg ist.
Wie können Speicherlecks verhindert werden? Die Hauptsache ist, dass wir vermeiden, unnötige Ressourcen zu behalten. Schauen wir uns einige häufige Szenarien an.
setInterval()
ruft wiederholt eine Funktion auf oder führt ein Codefragment aus, mit einer festen Zeitverzögerung zwischen den einzelnen Aufrufen. Es gibt eine Intervall ID
zurück ID
das Intervall eindeutig identifiziert, sodass Sie es später durch Aufrufen von clearInterval()
löschen können.
Wir erstellen eine Komponente, die eine Rückruffunktion aufruft, um anzuzeigen, dass sie nach x
Schleifen abgeschlossen ist. In diesem Beispiel verwende ich React, aber das funktioniert mit jedem FE-Framework.
import React, { useRef } from 'react'; const Timer = ({ cicles, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); zurückkehren; } currentCicles.current++; }, 500); zurückkehren ( <p>Laden...</p> ); } Standard-Timer exportieren;
Auf den ersten Blick scheint es kein Problem zu geben. Keine Sorge, erstellen wir eine weitere Komponente, die diesen Timer auslöst, und analysieren wir ihre Speicherleistung.
import React, { useState } from 'react'; Stile importieren aus „../styles/Home.module.css“ Timer aus '../components/Timer' importieren; Standardfunktion exportieren Home() { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); zurückkehren ( <p className={styles.container}> {showTimer ? ( <Timer cicles={10} onFinish={onFinish} /> ): ( <button onClick={() => setShowTimer(true)}> Wiederholen </button> )} </p> ) }
Nach ein paar Klicks auf die Schaltfläche Retry
sehen Sie hier das Ergebnis der Verwendung von Chrome Dev Tools zum Ermitteln der Speichernutzung:
Wenn wir auf die Schaltfläche „Wiederholen“ klicken, können wir sehen, dass immer mehr Speicher zugewiesen wird. Dies bedeutet, dass der zuvor zugewiesene Speicher nicht freigegeben wurde. Der Timer läuft noch, anstatt ersetzt zu werden.
Wie kann dieses Problem gelöst werden? Der Rückgabewert von setInterval
ist eine Intervall-ID, mit der wir dieses Intervall abbrechen können. In diesem speziellen Fall können wir clearInterval
aufrufen, nachdem die Komponente entladen wurde.
useEffect(() => { const IntervalId = setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); zurückkehren; } currentCicles.current++; }, 500); return () => clearInterval(intervalId); }, [])
Manchmal ist es schwierig, dieses Problem beim Schreiben von Code zu finden. Der beste Weg ist, die Komponenten zu abstrahieren.
Wenn wir hier React verwenden, können wir die gesamte Logik in einen benutzerdefinierten Hook einbinden.
import { useEffect } aus 'react'; export const useTimeout = (refreshCycle = 100, Rückruf) => { useEffect(() => { if (refreshCycle <= 0) { setTimeout(callback, 0); zurückkehren; } const IntervalId = setInterval(() => { Rückruf(); },freshCycle); return () => clearInterval(intervalId); }, [refreshCycle, setInterval, clearInterval]); }; Standard exportieren useTimeout;
Wann immer Sie setInterval
verwenden müssen, können Sie Folgendes tun:
const handleTimeout = () => ...; useTimeout(100, handleTimeout);
Jetzt können Sie diesen useTimeout Hook
verwenden, ohne sich Gedanken über Speicherverluste machen zu müssen, was auch der Vorteil der Abstraktion ist.
Die Web-API bietet eine große Anzahl von Ereignis-Listenern. Zuvor haben wir setTimeout
besprochen. Schauen wir uns nun addEventListener
an.
In diesem Beispiel erstellen wir eine Tastaturkürzelfunktion. Da wir auf verschiedenen Seiten unterschiedliche Funktionen haben, werden unterschiedliche Tastenkombinationsfunktionen erstellt
Funktion homeShortcuts({ key}) { if (key === 'E') { console.log('Widget bearbeiten') } } // Wenn sich der Benutzer auf der Homepage anmeldet, führen wir document.addEventListener('keyup', homeShortcuts); // Der Benutzer macht etwas und navigiert dann zur Einstellungsfunktion SettingsShortcuts({ key}) { if (key === 'E') { console.log('Einstellung bearbeiten') } } // Wenn sich der Benutzer auf der Homepage anmeldet, führen wir document.addEventListener('keyup', SettingsShortcuts);
Es sieht immer noch gut aus, außer dass die vorherige keyup
nicht bereinigt wird, wenn der zweite addEventListener
ausgeführt wird. Anstatt unseren keyup
Listener zu ersetzen, fügt dieser Code einen weiteren callback
hinzu. Das bedeutet, dass beim Drücken einer Taste zwei Funktionen ausgelöst werden.
Um den vorherigen Rückruf zu löschen, müssen wir removeEventListener
verwenden:
document.removeEventListener('keyup', homeShortcuts);
Refaktorieren Sie den obigen Code:
Funktion homeShortcuts({ key}) { if (key === 'E') { console.log('Widget bearbeiten') } } // Der Benutzer landet auf der Startseite und wir führen ihn aus document.addEventListener('keyup', homeShortcuts); // Der Benutzer führt einige Dinge aus und navigiert zu den Einstellungen FunktionseinstellungenShortcuts({ key}) { if (key === 'E') { console.log('Einstellung bearbeiten') } } // Der Benutzer landet auf der Startseite und wir führen ihn aus document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', SettingsShortcuts);
Als Faustregel gilt: Seien Sie sehr vorsichtig, wenn Sie Werkzeuge aus globalen Objekten verwenden.
Beobachter sind eine Browser-Web-API-Funktion, die vielen Entwicklern nicht bekannt ist. Dies ist hilfreich, wenn Sie nach Änderungen in der Sichtbarkeit oder Größe von HTML-Elementen suchen möchten.
Die IntersectionObserver
Schnittstelle (Teil der Intersection Observer-API) bietet eine Methode zum asynchronen Beobachten des Schnittstatus eines Zielelements mit seinen Vorgängerelementen oder viewport
der obersten Ebene. Das Vorgängerelement und viewport
werden als root
bezeichnet.
Obwohl es mächtig ist, müssen wir es mit Vorsicht verwenden. Wenn Sie mit der Beobachtung eines Objekts fertig sind, denken Sie daran, es abzubrechen, wenn es nicht verwendet wird.
Schauen Sie sich den Code an:
const ref = ... const sichtbar = (sichtbar) => { console.log(`Es ist ${visible}`); } useEffect(() => { if (!ref) { zurückkehren; } Observer.current = neuer IntersectionObserver( (Einträge) => { if (!entries[0].isIntersecting) { sichtbar(wahr); } anders { sichtbar(falsch); } }, { rootMargin: `-${header.height}px` }, ); Observer.current.observe(ref); }, [ref]);
Der obige Code sieht gut aus. Was passiert jedoch mit dem Beobachter, wenn die Komponente entladen wird? Sie wird nicht gelöscht und der Speicher geht verloren. Wie lösen wir dieses Problem? Verwenden Sie einfach die disconnect
:
const ref = ... const sichtbar = (sichtbar) => { console.log(`Es ist ${visible}`); } useEffect(() => { if (!ref) { zurückkehren; } Observer.current = neuer IntersectionObserver( (Einträge) => { if (!entries[0].isIntersecting) { sichtbar(wahr); } anders { sichtbar(falsch); } }, { rootMargin: `-${header.height}px` }, ); Observer.current.observe(ref); return () => Observer.current?.disconnect(); }, [ref]);
Das Hinzufügen von Objekten zu einem Fenster ist ein häufiger Fehler. In einigen Szenarien kann es schwierig sein, es zu finden, insbesondere wenn this
in einem Fensterausführungskontext verwendet wird. Schauen Sie sich das folgende Beispiel an:
Funktion addElement(element) { if (!this.stack) { this.stack = { Elemente: [] } } this.stack.elements.push(element); }
Es sieht harmlos aus, hängt aber davon ab, aus welchem Kontext Sie addElement
aufrufen. Wenn Sie addElement aus dem Fensterkontext aufrufen, wächst der Heap.
Ein weiteres Problem könnte darin bestehen, eine globale Variable falsch zu definieren:
var a = 'example 1'; // Der Bereich ist auf den Ort beschränkt, an dem var erstellt wird. b = 'example 2'; // Dem Window-Objekt hinzugefügt
Um dieses Problem zu vermeiden, können Sie den strikten Modus verwenden:
„streng verwenden“
Durch die Verwendung des strikten Modus signalisieren Sie dem JavaScript-Compiler, dass Sie sich vor diesen Verhaltensweisen schützen möchten. Sie können Windows bei Bedarf weiterhin verwenden. Sie müssen es jedoch explizit verwenden.
Wie sich der strikte Modus auf unser vorheriges Beispiel auswirkt:
Für die addElement
-Funktion ist this
undefiniert, wenn sie aus dem globalen Bereich aufgerufen wird
Wenn Sie const | let | var
nicht angeben, erhalten Sie die folgende Fehlermeldung:
Nicht erfasster Referenzfehler: b ist nicht definiert
Auch DOM-Knoten sind nicht immun gegen Speicherlecks. Wir müssen darauf achten, keine Verweise darauf zu speichern. Andernfalls kann der Garbage Collector sie nicht reinigen, da sie weiterhin zugänglich sind.
Demonstrieren Sie es mit einem kleinen Code:
const elements = []; const list = document.getElementById('list'); Funktion addElement() { // saubere Knoten list.innerHTML = ''; const pElement= document.createElement('p'); const element = document.createTextNode(`Element ${elements.length} hinzufügen`); pElement.appendChild(element); list.appendChild(pElement); elements.push(pElement); } document.getElementById('addElement').onclick = addElement;
Beachten Sie, dass die Funktion addElement
die Liste p
löscht und ihr ein neues Element als untergeordnetes Element hinzufügt. Dieses neu erstellte Element wird dem elements
hinzugefügt.
Bei der nächsten Ausführung addElement
wird das Element aus der Liste p
entfernt, ist jedoch nicht für die Garbage Collection geeignet, da es im elements
-Array gespeichert ist.
Wir überwachen die Funktion, nachdem wir sie einige Male ausgeführt haben:
Sehen Sie im Screenshot oben, wie der Knoten kompromittiert wurde. Wie kann man dieses Problem lösen? Durch das Löschen des elements
werden sie für die Garbage Collection geeignet.
In diesem Artikel haben wir uns die häufigsten Ursachen für Speicherverluste angesehen. Es ist offensichtlich, dass JavaScript selbst keinen Speicherverlust verursacht. Stattdessen wird es durch eine unbeabsichtigte Speicherretention seitens des Entwicklers verursacht. Solange der Code sauber ist und wir nicht vergessen, ihn selbst aufzuräumen, wird es keine Lecks geben.
Es ist ein Muss zu verstehen, wie Speicher und Garbage Collection in JavaScript funktionieren. Einige Entwickler haben den falschen Eindruck, dass sie sich über dieses Problem keine Sorgen machen müssen, da es automatisch erfolgt.