Entrée frontale (vue) au cours de compétence : entrer pour apprendre JavaScript ne fournit aucune opération de gestion de la mémoire. Au lieu de cela, la mémoire est gérée par la machine virtuelle JavaScript via un processus de récupération de mémoire appelé garbage collection .
Puisque nous ne pouvons pas forcer la collecte des déchets, comment pouvons-nous savoir si cela fonctionne ? Qu’en savons-nous ?
L'exécution du script est suspendue pendant ce processus
Il libère de la mémoire pour les ressources inaccessibles
c'est incertain
Il ne vérifie pas toute la mémoire en même temps, mais s'exécute en plusieurs cycles
C'est imprévisible mais il fonctionnera lorsque cela sera nécessaire
Cela signifie-t-il qu’il n’y a pas lieu de s’inquiéter des problèmes d’allocation de ressources et de mémoire ? Si nous n’y prenons pas garde, certaines fuites de mémoire peuvent survenir.
Une fuite de mémoire est un bloc de mémoire allouée que le logiciel ne peut pas récupérer.
Javascript fournit un garbage collector, mais cela ne signifie pas que nous pouvons éviter les fuites de mémoire. Pour être éligible au garbage collection, l’objet ne doit pas être référencé ailleurs. Si vous détenez des références à des ressources inutilisées, cela empêchera la récupération de ces ressources. C'est ce qu'on appelle la rétention de mémoire inconsciente .
Une fuite de mémoire peut entraîner une exécution plus fréquente du garbage collector. Étant donné que ce processus empêchera l'exécution du script, cela peut entraîner le blocage de notre programme. Si un tel décalage se produit, les utilisateurs exigeants remarqueront certainement que s'ils n'en sont pas satisfaits, le produit sera hors ligne pendant une longue période. Plus sérieusement, cela peut provoquer le crash de l'ensemble de l'application, ce qui est gg.
Comment éviter les fuites de mémoire ? L’essentiel est d’éviter de conserver des ressources inutiles. Examinons quelques scénarios courants.
setInterval()
appelle à plusieurs reprises une fonction ou exécute un fragment de code, avec un délai fixe entre chaque appel. Il renvoie un ID
d'intervalle ID
identifie de manière unique l'intervalle afin que vous puissiez le supprimer ultérieurement en appelant clearInterval()
.
Nous créons un composant qui appelle une fonction de rappel pour indiquer qu'elle est terminée après x
nombre de boucles. J'utilise React dans cet exemple, mais cela fonctionne avec n'importe quel framework FE.
importer React, { useRef } depuis 'react' ; const Timer = ({ cycles, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); retour; } currentCicles.current++; }, 500); retour ( <p>Chargement...</p> ); } exporter la minuterie par défaut ;
À première vue, il ne semble y avoir aucun problème. Ne vous inquiétez pas, créons un autre composant qui déclenche ce timer et analysons ses performances en mémoire.
importer React, {useState} depuis 'react' ; importer des styles depuis '../styles/Home.module.css' importer la minuterie depuis '../components/Timer' ; exporter la fonction par défaut Home() { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); retour ( <p className={styles.container}> {showTimer ? <Timer cicles={10} onFinish={onFinish} /> ): ( <bouton onClick={() => setShowTimer(true)}> Réessayer </bouton> )} </p> ) }
Après quelques clics sur le bouton Retry
, voici le résultat de l'utilisation de Chrome Dev Tools pour obtenir l'utilisation de la mémoire :
Lorsque nous cliquons sur le bouton Réessayer, nous pouvons voir que de plus en plus de mémoire est allouée. Cela signifie que la mémoire précédemment allouée n'a pas été libérée. La minuterie fonctionne toujours au lieu d'être remplacée.
Comment résoudre ce problème ? La valeur de retour de setInterval
est un identifiant d'intervalle, que nous pouvons utiliser pour annuler cet intervalle. Dans ce cas particulier, nous pouvons appeler clearInterval
après le déchargement du composant.
utiliserEffet(() => { const intervalId = setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); retour; } currentCicles.current++; }, 500); return () => clearInterval(intervalId); }, [])
Parfois, il est difficile de trouver ce problème lors de l’écriture du code. La meilleure façon est d’abstraire les composants.
En utilisant React ici, nous pouvons envelopper toute cette logique dans un Hook personnalisé.
importer { useEffect } depuis 'react' ; export const useTimeout = (refreshCycle = 100, rappel) => { utiliserEffet(() => { si (refreshCycle <= 0) { setTimeout(rappel, 0); retour; } const intervalId = setInterval(() => { rappel(); }, rafraîchissementCycle); return () => clearInterval(intervalId); }, [refreshCycle, setInterval, clearInterval]); } ; exporter useTimeout par défaut ;
Désormais, chaque fois que vous devez utiliser setInterval
, vous pouvez faire ceci :
const handleTimeout = () => ...; useTimeout(100, handleTimeout);
Vous pouvez désormais utiliser ce useTimeout Hook
sans vous soucier des fuites de mémoire, ce qui constitue également l'avantage de l'abstraction.
L'API Web fournit un grand nombre d'écouteurs d'événements. Plus tôt, nous avons discuté de setTimeout
. Regardons maintenant addEventListener
.
Dans cet exemple, nous créons une fonction de raccourci clavier. Puisque nous avons différentes fonctions sur différentes pages, différentes fonctions de touches de raccourci seront créées
function homeShortcuts({ clé}) { if (clé === 'E') { console.log('modifier le widget') } } // Lorsque l'utilisateur se connecte sur la page d'accueil, nous exécutons document.addEventListener('keyup', homeShortcuts); // L'utilisateur fait quelque chose puis accède à la fonction de paramètres settingsShortcuts({ key}) { if (clé === 'E') { console.log('modifier le paramètre') } } // Lorsque l'utilisateur se connecte sur la page d'accueil, nous exécutons document.addEventListener('keyup', settingsShortcuts);
Cela semble toujours bien, sauf que la keyup
précédente n'est pas nettoyée lors de l'exécution du deuxième addEventListener
. Plutôt que de remplacer notre écouteur keyup
, ce code ajoutera un autre callback
. Cela signifie que lorsqu'une touche est enfoncée, elle déclenche deux fonctions.
Pour effacer le rappel précédent, nous devons utiliser removeEventListener
:
document.removeEventListener('keyup', homeShortcuts);
Refactorisez le code ci-dessus :
function homeShortcuts({ clé}) { if (clé === 'E') { console.log('modifier le widget') } } // l'utilisateur arrive à la maison et nous exécutons document.addEventListener('keyup', homeShortcuts); // l'utilisateur fait certaines choses et accède aux paramètres paramètres de fonctionRaccourcis({ clé}) { if (clé === 'E') { console.log('modifier le paramètre') } } // l'utilisateur arrive à la maison et nous exécutons document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', settingsShortcuts);
En règle générale, soyez très prudent lorsque vous utilisez des outils issus d’objets globaux.
Les observateurs sont une fonctionnalité de l'API Web du navigateur que de nombreux développeurs ignorent. Ceci est puissant si vous souhaitez vérifier les changements de visibilité ou de taille des éléments HTML.
L'interface IntersectionObserver
(qui fait partie de l'API Intersection Observer) fournit une méthode pour observer de manière asynchrone l'état d'intersection d'un élément cible avec ses éléments ancêtres ou viewport
de document de niveau supérieur. L'élément ancêtre et viewport
sont appelés root
.
Bien qu’il soit puissant, il faut l’utiliser avec prudence. Une fois que vous avez fini d'observer un objet, pensez à l'annuler lorsqu'il n'est pas utilisé.
Jetez un oeil au code:
référence const = ... const visible = (visible) => { console.log(`C'est ${visible}`); } utiliserEffet(() => { si (!ref) { retour; } observer.current = new IntersectionObserver( (entrées) => { si (!entries[0].isIntersecting) { visible(vrai); } autre { visible(faux); } }, { rootMargin : `-${header.height}px` }, ); observer.current.observer(réf); }, [réf]);
Le code ci-dessus semble correct. Cependant, qu'arrive-t-il à l'observateur une fois le composant déchargé ? Il n'est pas effacé et la mémoire est perdue. Comment pouvons-nous résoudre ce problème ? Utilisez simplement la méthode disconnect
:
référence const = ... const visible = (visible) => { console.log(`C'est ${visible}`); } utiliserEffet(() => { si (!ref) { retour; } observer.current = new IntersectionObserver( (entrées) => { si (!entries[0].isIntersecting) { visible(vrai); } autre { visible(faux); } }, { rootMargin : `-${header.height}px` }, ); observer.current.observer(réf); return () => observer.current?.disconnect(); }, [réf]);
L'ajout d'objets à une fenêtre est une erreur courante. Dans certains scénarios, il peut être difficile de le trouver, notamment lors de l'utilisation this
dans un contexte d'exécution de fenêtre. Jetez un œil à l’exemple suivant :
fonction addElement (élément) { si (!this.stack) { ceci.stack = { éléments : [] } } this.stack.elements.push(element); }
Cela semble inoffensif, mais cela dépend du contexte à partir duquel vous appelez addElement
. Si vous appelez addElement depuis le contexte de fenêtre, le tas augmentera.
Un autre problème pourrait être la définition incorrecte d'une variable globale :
var a = 'exemple 1'; // La portée est limitée à l'endroit où var est créé b = 'exemple 2'; // Ajouté à l'objet Window;
Pour éviter ce problème, vous pouvez utiliser le mode strict :
"utiliser strictement"
En utilisant le mode strict, vous signalez au compilateur JavaScript que vous souhaitez vous protéger de ces comportements. Vous pouvez toujours utiliser Windows lorsque vous en avez besoin. Cependant, vous devez l'utiliser de manière explicite.
Comment le mode strict affecte notre exemple précédent :
Pour la fonction addElement
, this
n'est pas défini lorsqu'il est appelé depuis la portée globale
Si vous ne spécifiez pas const | let | var
sur une variable, vous obtiendrez l'erreur suivante :
Uncaught ReferenceError : b n'est pas défini
Les nœuds DOM ne sont pas non plus à l’abri des fuites de mémoire. Nous devons faire attention à ne pas enregistrer de références à ces éléments. Dans le cas contraire, le garbage collector ne pourra pas les nettoyer car ils seront toujours accessibles.
Démontrez-le avec un petit morceau de code :
éléments const = []; const list = document.getElementById('list'); fonction addElement() { // nettoyer les nœuds liste.innerHTML = ''; const pElement= document.createElement('p'); const element = document.createTextNode(`ajout d'un élément ${elements.length}`); pElement.appendChild(élément); list.appendChild(pElement); elements.push(pElement); } document.getElementById('addElement').onclick = addElement;
Notez que la fonction addElement
efface la liste p
et y ajoute un nouvel élément en tant qu'élément enfant. Cet élément nouvellement créé est ajouté au tableau elements
.
La prochaine fois que addElement
sera exécuté, l'élément sera supprimé de la liste p
, mais il ne convient pas au garbage collection car il est stocké dans le tableau elements
.
Nous surveillons la fonction après l'avoir exécutée plusieurs fois :
Voyez comment le nœud a été compromis dans la capture d'écran ci-dessus. Alors comment résoudre ce problème ? Effacer le tableau elements
les rendra éligibles au garbage collection.
Dans cet article, nous avons examiné les méthodes les plus courantes de fuite de mémoire. Il est évident que JavaScript lui-même ne perd pas de mémoire. Au lieu de cela, cela est dû à une rétention de mémoire involontaire de la part du développeur. Tant que le code est propre et que nous n'oublions pas de nettoyer après nous-mêmes, les fuites ne se produiront pas.
Comprendre le fonctionnement de la mémoire et du garbage collection en JavaScript est indispensable. Certains développeurs ont la fausse impression que puisque c'est automatique, ils n'ont pas à s'inquiéter de ce problème.