Maison > interface Web > js tutoriel > le corps du texte

Comment éviter les fuites de mémoire dans JS ? Parlons de 5 erreurs de mémoire courantes

青灯夜游
Libérer: 2021-12-31 10:57:28
avant
1867 Les gens l'ont consulté

Comment éviter les fuites de mémoire dans JS ? Parlons de 5 erreurs de mémoire courantes

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 elle fonctionne correctement ? 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
  • Il est non déterministe
  • Il ne vérifie pas toute la mémoire en même temps, mais s'exécute en plusieurs cycles
  • Il est non déterministe Prédictif , mais il s'exécutera si 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. Bien sûr que non ? Si nous n’y prenons pas garde, certaines fuites de mémoire peuvent survenir.

Comment éviter les fuites de mémoire dans JS ? Parlons de 5 erreurs de mémoire courantes

Qu'est-ce qu'une fuite de mémoire ?

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, notre programme pourrait rester bloqué. Si un tel retard se produit, les utilisateurs exigeants remarqueront certainement que s'ils n'en sont pas satisfaits, le produit ne sera pas 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.

1. La méthode de surveillance du minuteur

setInterval() appelle à plusieurs reprises une fonction ou exécute un fragment de code, avec un délai fixe entre chaque appel. Il renvoie un intervalle ID qui identifie de manière unique l'intervalle afin que vous puissiez le supprimer ultérieurement en appelant clearInterval(). setInterval() 方法重复调用函数或执行代码片段,每次调用之间有固定的时间延迟。它返回一个时间间隔ID,该ID唯一地标识时间间隔,因此您可以稍后通过调用 clearInterval() 来删除它。

我们创建一个组件,它调用一个回调函数来表示它在x个循环之后完成了。我在这个例子中使用React,但这适用于任何FE框架。

import React, { useRef } from 'react';

const Timer = ({ cicles, onFinish }) => {
    const currentCicles = useRef(0);

    setInterval(() => {
        if (currentCicles.current >= cicles) {
            onFinish();
            return;
        }
        currentCicles.current++;
    }, 500);

    return (
        <div>Loading ...</div>
    );
}

export default Timer;
Copier après la connexion

一看,好像没啥问题。不急,我们再创建一个触发这个定时器的组件,并分析其内存性能。

import React, { useState } from &#39;react&#39;;
import styles from &#39;../styles/Home.module.css&#39;
import Timer from &#39;../components/Timer&#39;;

export default function Home() {
    const [showTimer, setShowTimer] = useState();
    const onFinish = () => setShowTimer(false);

    return (
      <div className={styles.container}>
          {showTimer ? (
              <Timer cicles={10} onFinish={onFinish} />
          ): (
              <button onClick={() => setShowTimer(true)}>
                Retry
              </button>
          )}
      </div>
    )
}
Copier après la connexion

Retry 按钮上单击几次后,这是使用Chrome Dev Tools获取内存使用的结果:

Comment éviter les fuites de mémoire dans JS ? Parlons de 5 erreurs de mémoire courantes

当我们点击重试按钮时,可以看到分配的内存越来越多。这说明之前分配的内存没有被释放。计时器仍然在运行而不是被替换。

怎么解决这个问题?setInterval 的返回值是一个间隔 ID,我们可以用它来取消这个间隔。在这种特殊情况下,我们可以在组件卸载后调用 clearInterval

useEffect(() => {
    const intervalId = setInterval(() => {
        if (currentCicles.current >= cicles) {
            onFinish();
            return;
        }
        currentCicles.current++;
    }, 500);

    return () => clearInterval(intervalId);
}, [])
Copier après la connexion

有时,在编写代码时,很难发现这个问题,最好的方式,还是要把组件抽象化。

这里使用的是React,我们可以把所有这些逻辑都包装在一个自定义的 Hook 中。

import { useEffect } from &#39;react&#39;;

export const useTimeout = (refreshCycle = 100, callback) => {
    useEffect(() => {
        if (refreshCycle <= 0) {
            setTimeout(callback, 0);
            return;
        }

        const intervalId = setInterval(() => {
            callback();
        }, refreshCycle);

        return () => clearInterval(intervalId);
    }, [refreshCycle, setInterval, clearInterval]);
};

export default useTimeout;
Copier après la connexion

现在需要使用setInterval时,都可以这样做:

const handleTimeout = () => ...;

useTimeout(100, handleTimeout);
Copier après la connexion

现在你可以使用这个useTimeout Hook,而不必担心内存被泄露,这也是抽象化的好处。

2.事件监听

Web API提供了大量的事件监听器。在前面,我们讨论了setTimeout。现在来看看 addEventListener

在这个事例中,我们创建一个键盘快捷键功能。由于我们在不同的页面上有不同的功能,所以将创建不同的快捷键功能

function homeShortcuts({ key}) {
    if (key === &#39;E&#39;) {
        console.log(&#39;edit widget&#39;)
    }
}

// 用户在主页上登陆,我们执行
document.addEventListener(&#39;keyup&#39;, homeShortcuts); 


// 用户做一些事情,然后导航到设置

function settingsShortcuts({ key}) {
    if (key === &#39;E&#39;) {
        console.log(&#39;edit setting&#39;)
    }
}

// 用户在主页上登陆,我们执行
document.addEventListener(&#39;keyup&#39;, settingsShortcuts);
Copier après la connexion

看起来还是很好,除了在执行第二个 addEventListener 时没有清理之前的 keyup。这段代码不是替换我们的 keyup 监听器,而是将添加另一个 callback。这意味着,当一个键被按下时,它将触发两个函数。

要清除之前的回调,我们需要使用 removeEventListener

Nous créons un composant qui appelle une fonction de rappel pour indiquer qu'elle est terminée après les boucles x. J'utilise React dans cet exemple, mais cela fonctionne avec n'importe quel framework FE.

document.removeEventListener(‘keyup’, homeShortcuts);
Copier après la connexion

À 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.

function homeShortcuts({ key}) {
    if (key === &#39;E&#39;) {
        console.log(&#39;edit widget&#39;)
    }
}

// user lands on home and we execute
document.addEventListener(&#39;keyup&#39;, homeShortcuts); 


// user does some stuff and navigates to settings

function settingsShortcuts({ key}) {
    if (key === &#39;E&#39;) {
        console.log(&#39;edit setting&#39;)
    }
}

// user lands on home and we execute
document.removeEventListener(&#39;keyup&#39;, homeShortcuts); 
document.addEventListener(&#39;keyup&#39;, settingsShortcuts);
Copier après la connexion

Après quelques clics sur le bouton Réessayer, voici le résultat de l'utilisation de Chrome Dev Tools pour obtenir l'utilisation de la mémoire :

Comment éviter les fuites de mémoire dans JS ? Parlons de 5 erreurs de mémoire courantesLorsque 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 l'intervalle. Dans ce cas particulier, nous pouvons appeler clearInterval après le déchargement du composant.

const ref = ...
const visible = (visible) => {
  console.log(`It is ${visible}`);
}

useEffect(() => {
    if (!ref) {
        return;
    }

    observer.current = new IntersectionObserver(
        (entries) => {
            if (!entries[0].isIntersecting) {
                visible(true);
            } else {
                visbile(false);
            }
        },
        { rootMargin: `-${header.height}px` },
    );

    observer.current.observe(ref);
}, [ref]);
Copier après la connexion
Copier après la connexion
Parfois, il est difficile de trouver ce problème lors de l'écriture de code. Le meilleur moyen est d'abstraire les composants.

React est utilisé ici, et nous pouvons envelopper toute cette logique dans un Hook personnalisé. 🎜
const ref = ...
const visible = (visible) => {
  console.log(`It is ${visible}`);
}

useEffect(() => {
    if (!ref) {
        return;
    }

    observer.current = new IntersectionObserver(
        (entries) => {
            if (!entries[0].isIntersecting) {
                visible(true);
            } else {
                visbile(false);
            }
        },
        { rootMargin: `-${header.height}px` },
    );

    observer.current.observe(ref);

    return () => observer.current?.disconnect();
}, [ref]);
Copier après la connexion
Copier après la connexion
🎜Maintenant, chaque fois que vous avez besoin d'utiliser setInterval, vous pouvez faire ceci : 🎜
function addElement(element) {
    if (!this.stack) {
        this.stack = {
            elements: []
        }
    }

    this.stack.elements.push(element);
}
Copier après la connexion
Copier après la connexion
🎜Vous pouvez désormais utiliser ce useTimeout Hook sans vous soucier d'une fuite de mémoire. avantages de l'abstraction. 🎜🎜🎜🎜2. Écoute d'événements🎜🎜🎜🎜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 🎜
var a = &#39;example 1&#39;; // 作用域限定在创建var的地方
b = &#39;example 2&#39;; // 添加到Window对象中
Copier après la connexion
Copier après la connexion
🎜 Ça a toujours l'air bien, sauf que le addEventListener >keyup. 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 :🎜
"use strict"
Copier après la connexion
Copier après la connexion
🎜Refactorisez le code ci-dessus : 🎜
Uncaught ReferenceError: b is not defined
Copier après la connexion
Copier après la connexion
🎜Selon l'expérience, lorsque vous utilisez des outils à partir d'objets globaux, vous devez être très prudent. 🎜🎜🎜🎜3.Observers🎜🎜🎜🎜🎜Observers🎜 est une fonction API Web de navigateur que de nombreux développeurs ne connaissent pas. Ceci est puissant si vous souhaitez vérifier les changements de visibilité ou de taille des éléments HTML. 🎜

IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

尽管它很强大,但我们也要谨慎的使用它。一旦完成了对对象的观察,就要记得在不用的时候取消它。

看看代码:

const ref = ...
const visible = (visible) => {
  console.log(`It is ${visible}`);
}

useEffect(() => {
    if (!ref) {
        return;
    }

    observer.current = new IntersectionObserver(
        (entries) => {
            if (!entries[0].isIntersecting) {
                visible(true);
            } else {
                visbile(false);
            }
        },
        { rootMargin: `-${header.height}px` },
    );

    observer.current.observe(ref);
}, [ref]);
Copier après la connexion
Copier après la connexion

上面的代码看起来不错。然而,一旦组件被卸载,观察者会发生什么?它不会被清除,那内存可就泄漏了。我们怎么解决这个问题呢?只需要使用 disconnect 方法:

const ref = ...
const visible = (visible) => {
  console.log(`It is ${visible}`);
}

useEffect(() => {
    if (!ref) {
        return;
    }

    observer.current = new IntersectionObserver(
        (entries) => {
            if (!entries[0].isIntersecting) {
                visible(true);
            } else {
                visbile(false);
            }
        },
        { rootMargin: `-${header.height}px` },
    );

    observer.current.observe(ref);

    return () => observer.current?.disconnect();
}, [ref]);
Copier après la connexion
Copier après la connexion

4. Window Object

向 Window 添加对象是一个常见的错误。在某些场景中,可能很难找到它,特别是在使用 Window Execution上下文中的this关键字。看看下面的例子:

function addElement(element) {
    if (!this.stack) {
        this.stack = {
            elements: []
        }
    }

    this.stack.elements.push(element);
}
Copier après la connexion
Copier après la connexion

它看起来无害,但这取决于你从哪个上下文调用addElement。如果你从Window Context调用addElement,那就会越堆越多。

另一个问题可能是错误地定义了一个全局变量:

var a = &#39;example 1&#39;; // 作用域限定在创建var的地方
b = &#39;example 2&#39;; // 添加到Window对象中
Copier après la connexion
Copier après la connexion

要防止这种问题可以使用严格模式:

"use strict"
Copier après la connexion
Copier après la connexion

通过使用严格模式,向JavaScript编译器暗示,你想保护自己免受这些行为的影响。当你需要时,你仍然可以使用Window。不过,你必须以明确的方式使用它。

严格模式是如何影响我们前面的例子:

  • 对于 addElement 函数,当从全局作用域调用时,this 是未定义的
  • 如果没有在一个变量上指定const | let | var,你会得到以下错误:
Uncaught ReferenceError: b is not defined
Copier après la connexion
Copier après la connexion

5. 持有DOM引用

DOM节点也不能避免内存泄漏。我们需要注意不要保存它们的引用。否则,垃圾回收器将无法清理它们,因为它们仍然是可访问的。

用一小段代码演示一下:

const elements = [];
const list = document.getElementById(&#39;list&#39;);

function addElement() {
    // clean nodes
    list.innerHTML = &#39;&#39;;

    const divElement= document.createElement(&#39;div&#39;);
    const element = document.createTextNode(`adding element ${elements.length}`);
    divElement.appendChild(element);


    list.appendChild(divElement);
    elements.push(divElement);
}

document.getElementById(&#39;addElement&#39;).onclick = addElement;
Copier après la connexion

注意,addElement 函数清除列表 div,并将一个新元素作为子元素添加到它中。这个新创建的元素被添加到 elements 数组中。

下一次执行 addElement 时,该元素将从列表 div 中删除,但是它不适合进行垃圾收集,因为它存储在 elements 数组中。

我们在执行几次之后监视函数:

Comment éviter les fuites de mémoire dans JS ? Parlons de 5 erreurs de mémoire courantes

在上面的截图中看到节点是如何被泄露的。那怎么解决这个问题?清除 elements  数组将使它们有资格进行垃圾收集。

总结

在这篇文章中,我们已经看到了最常见的内存泄露方式。很明显,JavaScript本身并没有泄漏内存。相反,它是由开发者方面无意的内存保持造成的。只要代码是整洁的,而且我们不忘自己清理,就不会发生泄漏。

了解内存和垃圾回收在JavaScript中是如何工作的是必须的。一些开发者得到了错误的意识,认为由于它是自动的,所以他们不需要担心这个问题。

原文地址:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2

作者: Jose Granja

译者:前端小智

【相关推荐:javascript学习教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:segmentfault.com
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!