Maison  >  Article  >  interface Web  >  Fuites de mémoire JavaScript courantes

Fuites de mémoire JavaScript courantes

小云云
小云云original
2017-12-05 16:31:451720parcourir

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

Une fuite de mémoire fait référence à un programme qui ne parvient pas à être publié en raison d'une négligence ou une erreur et n'est plus utilisé de mémoire. Les fuites de mémoire ne font pas référence à la disparition physique de la mémoire, mais plutôt au fait qu'après que l'application ait alloué un certain segment de mémoire, en raison d'une erreur de conception, elle perd le contrôle du segment de mémoire avant qu'il ne soit libéré, provoquant ainsi un problème. gaspillage de mémoire.
Les fuites de mémoire ne peuvent généralement être analysées que par les programmeurs ayant accès au code source du programme. Cependant, de nombreuses personnes ont l'habitude de décrire toute augmentation indésirable de l'utilisation de la mémoire comme une fuite de mémoire, même si cela n'est pas strictement exact.
————wikipedia

Variable globale inattendue

Comment JavaScript gère les variables non déclarées : dans le global Créer une référence à la variable sur l'objet (c'est-à-dire une propriété sur l'objet global, pas une variable, car elle peut être supprimée via delete). Si vous êtes dans un navigateur, l'objet global est l'objet fenêtre.

Si les variables non déclarées mettent en cache une grande quantité de données, les données ne peuvent être libérées que lorsque la fenêtre est fermée ou que la page est actualisée. Cela peut provoquer des fuites de mémoire inattendues.

<span style="font-size: 14px;">function foo(arg) {<br>    bar = "this is a hidden global variable with a large of data";<br>}<br></span>

équivaut à :

<span style="font-size: 14px;">function foo(arg) {<br>    window.bar = "this is an explicit global variable with a large of data";<br>}<br></span>

De plus, des variables globales inattendues sont créées via ceci :

<span style="font-size: 14px;">function foo() {<br>    this.variable = "potential accidental global";<br>}<br><br>// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'<br>foo();<br></span>

Solution :

Ajoutez « use strict » dans le fichier JavaScript et activez le mode strict, ce qui peut efficacement éviter les problèmes ci-dessus.

<span style="font-size: 14px;">function foo(arg) {<br>    "use strict" // 在foo函数作用域内开启严格模式<br>    bar = "this is an explicit global variable with a large of data";// 报错:因为bar还没有被声明<br>}<br></span>

Si vous devez utiliser des variables globales dans une fonction, vous pouvez les déclarer explicitement dans la fenêtre comme indiqué dans le code suivant :

<span style="font-size: 14px;">function foo(arg) {<br>    window.bar = "this is a explicit global variable with a large of data";<br>}<br></span>

Ceci est non seulement très lisible, mais également pratique pour une maintenance ultérieure

En ce qui concerne les variables globales, vous devez faire attention à ces variables globales utilisé pour stocker temporairement de grandes quantités de données et assurez-vous de le définir sur null ou de le réaffecter après le traitement de ces données. Les variables globales sont également couramment utilisées comme cache. Généralement, le cache est utilisé pour l'optimisation des performances, il est préférable de définir une limite supérieure sur la taille du cache. Étant donné que le cache ne peut pas être récupéré, un cache plus élevé entraînera une consommation de mémoire plus élevée.

console.log

console.log : imprime un message sur la console de développement Web, souvent utilisé pour le débogage et l'analyse lors développement . Parfois, pendant le développement, vous devez imprimer certaines informations sur les objets, mais oubliez de supprimer l'instruction console.log lors de la publication, ce qui peut provoquer des fuites de mémoire.

L'objet transmis à console.log ne peut pas être récupéré ♻️ car l'outil de développement doit pouvoir afficher les informations sur l'objet après l'exécution du code. Il est donc préférable de ne pas consoler.enregistrer aucun objet dans l'environnement de production.

Exemple------>demos/log.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Leaker</title><br></head><br><br><body><br>  <input type="button" value="click"><br>  <script><br>    !function () {<br>      function Leaker() {<br>        this.init();<br>      };<br>      Leaker.prototype = {<br>        init: function () {<br>          this.name = (Array(100000)).join('*');<br>          console.log("Leaking an object %o: %o", (new Date()), this);// this对象不能被回收<br>        },<br><br>        destroy: function () {<br>          // do something....<br>        }<br>      };<br>      document.querySelector('input').addEventListener('click', function () {<br>        new Leaker();<br>      }, false);<br>    }()<br>  </script><br></body><br><br></html><br></span>

Ici combiné avec les outils de développement de Chrome–>Performances Effectuez une analyse. Les étapes sont les suivantes :

⚠️Remarque : Il est préférable d'effectuer l'analyse dans une fenêtre masquée pour éviter que les plug-ins du navigateur n'affectent les résultats de l'analyse

  1. Ouvrez l'enregistrement de l'élément [Performance]

  2. Exécutez CG une fois et créez une ligne de référence de base

  3. Cliquez trois fois de suite sur le bouton [cliquez] pour créer trois nouveaux objets Leaker

  4. Exécuter CG une fois

  5. Arrêter l'enregistrement

Fuites de mémoire JavaScript courantes

On voit que la ligne [JS Heap] est à la fin. Sans revenir à la ligne de référence de base, il y a évidemment de la mémoire qui n'a pas été récupérée. Si vous modifiez le code pour :

<span style="font-size: 14px;">    !function () {<br>      function Leaker() {<br>        this.init();<br>      };<br>      Leaker.prototype = {<br>        init: function () {<br>          this.name = (Array(100000)).join('*');<br>        },<br><br>        destroy: function () {<br>          // do something....<br>        }<br>      };<br>      document.querySelector('input').addEventListener('click', function () {<br>        new Leaker();<br>      }, false);<br>    }()<br></span>

supprimez la console.log("Leaking an object %o: %o", (new Date()), this instruction); . Répétez les étapes ci-dessus, et les résultats de l'analyse sont les suivants :

Fuites de mémoire JavaScript courantes

D'après les résultats de l'analyse comparative, on peut voir que les objets imprimés par la console .log ne sera pas récupéré. Par conséquent, il est préférable de ne pas consoler.enregistrer des objets volumineux dans la page, car cela pourrait affecter les performances globales de la page, en particulier dans un environnement de production. En plus de console.log, il existe également des problèmes similaires tels que console.dir, console.error, console.warn, etc. Ces détails nécessitent une attention particulière.

closures(闭包)

当一个函数A返回一个内联函数B,即使函数A执行完,函数B也能访问函数A作用域内的变量,这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。

<span style="font-size: 14px;">function foo(message) {<br>    function closure() {<br>        console.log(message)<br>    };<br>    return closure;<br>}<br><br>// 使用<br>var bar = foo("hello closure!");<br>bar()// 返回 'hello closure!'<br></span>

在函数foo内创建的函数closure对象是不能被回收掉的,因为它被全局变量bar引用,处于一直可访问状态。通过执行bar()可以打印出hello closure!。如果想释放掉可以将bar = null即可。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

实例------>demos/closures.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Closure</title><br></head><br><br><body><br>  <p>不断单击【click】按钮</p><br>  <button id="click_button">Click</button><br>  <script><br>    function f() {<br>      var str = Array(10000).join('#');<br>      var foo = {<br>        name: 'foo'<br>      }<br>      function unused() {<br>        var message = 'it is only a test message';<br>        str = 'unused: ' + str;<br>      }<br>      function getData() {<br>        return 'data';<br>      }<br>      return getData;<br>    }<br><br>    var list = [];<br>    <br>    document.querySelector('#click_button').addEventListener('click', function () {<br>      list.push(f());<br>    }, false);<br>  </script><br></body><br><br></html><br></span>

这里结合Chrome的Devtools->Memory工具进行分析,操作步骤如下:

⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 选中【Record allocation timeline】选项

  2. 执行一次CG

  3. 单击【start】按钮开始记录堆分析

  4. 连续单击【click】按钮十多次

  5. 停止记录堆分析

Fuites de mémoire JavaScript courantes

上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条,过滤出对应新分配的对象:

Fuites de mémoire JavaScript courantes

查看对象的详细信息:

Fuites de mémoire JavaScript courantes

从图可知,在返回的闭包作用链(Scopes)中携带有它所在函数的作用域,作用域中还包含一个str字段。而str字段并没有在返回getData()中使用过。为什么会存在在作用域中,按理应该被GC回收掉, why

原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因,便可以对症下药了。对代码做如下修改:

<span style="font-size: 14px;">    function f() {<br>      var str = Array(10000).join('#');<br>      var foo = {<br>        name: 'foo'<br>      }<br>      function unused() {<br>        var message = 'it is only a test message';<br>        // str = 'unused: ' + str; //删除该条语句<br>      }<br>      function getData() {<br>        return 'data';<br>      }<br>      return getData;<br>    }<br><br>    var list = [];<br>    <br>    document.querySelector('#click_button').addEventListener('click', function () {<br>      list.push(f());<br>    }, false);<br></span>

getData()和unused()内部函数共享f函数对应的变量对象,因为unused()内部函数访问了f作用域内str变量,所以str字段存在于f变量对象中。加上getData()内部函数被返回,被其他对象引用,形成了闭包,因此对应的f变量对象存在于闭包函数的作用域链中。这里只要将函数unused中str = 'unused: ' + str;语句删除便可解决问题。

Fuites de mémoire JavaScript courantes

查看一下闭包信息:

Fuites de mémoire JavaScript courantes

DOM泄露

在JavaScript中,DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。如Chrome浏览器中DOM位于WebCore,而JavaScript/ECMAScript位于V8中。假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大影响。了解更多ℹ️

Fuites de mémoire JavaScript courantes

为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。

实例------>demos/dom.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Dom-Leakage</title><br></head><br><body><br>  <input type="button" value="remove" class="remove"><br>  <input type="button" value="add" class="add"><br><br>  <p class="container"><br>    <pre class="wrapper">