ShirlyChenLaLaLa / ShirlyChenLaLaLa.github.io

学习前端的一些记录
Other
1 stars 0 forks source link

Js 如何避免内存泄露(笔记) #11

Open ShirlyChenLaLaLa opened 5 years ago

ShirlyChenLaLaLa commented 5 years ago

Js 如何避免内存泄露(笔记)

内存泄露的定义

应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。

常见的内存泄露

1. 意外的全局变量
function foo(arg) {
    bar = "this is a hidden global variable";
}
function foo() {
    this.variable = "potential accidental global";
}

JavaScript对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过delete删除)。 未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。与全局变量相关的增加内存消耗的一个主因是缓存。高内存消耗导致缓存突破上限,因为缓存内容无法被回收。

解决方法: 在JavaScript文件中添加'use strict',开启严格模式,可以有效地避免上述问题。


2. 被遗忘的计时器或回调函数

老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。如今,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经可以正确检测和处理循环引用了。换言之,回收节点内存时,不必非要调用 removeEventListener 了。

被遗忘的计时器示例:

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

node 对象一旦被删除,而计时器却仍然没被回收,同时someResource存储了大量数据的话,也无法被回收。


3. 闭包

闭包:当一个函数A返回一个内联函数B,即使函数A执行完,函数B也能访问函数A作用域内的变量

function foo(message) {
    function closure() {
        console.log(message)
    };
    return closure;
}

// 使用
var bar = foo("hello closure!");
bar()// 打印 'hello closure!'

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

如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。

同样,我们可以使得闭包函数作用域中没有包含函数对应的变量对象。对于以上例子就是不引用message,直接console.log('haha')


4. DOM泄露 (一般来说是IE<8)

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

在IE<8版本中,JScript垃圾回收器仅管理JScript对象生命周期而不会管理DOM对象的(即DOM对象有自己的垃圾回收器)。因此JScript回收器不会解除掉DOM对象与Jscript对象之间的相互引用,这从而导致内存泄露。 在IE6中,循环引用只在IE浏览器程序退出时才会被解除,而在IE7中,离开当前页面时,才会解除页面中的循环引用。IE8修复该问题,JScript垃圾回收器会将引用的DOM对象视为JScript对象,从而避免循环引用不能被解除的问题(注:这里循环引用解除是指浏览器自动解除循环引用)。

在IE9把BOM和DOM对象转换为真正的js对象。

<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"   content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Dom-Leakage</title>
    </head>
    <body>
        <input type="button" value="remove" class="remove">
        <input type="button" value="add" class="add">

        <div class="container">
        <pre class="wrapper"></pre>
        </div>
        <script>
        // 因为要多次用到<pre>节点,将其缓存到本地变量wrapper中,
        var wrapper = document.querySelector('.wrapper');
        var counter = 0;

        document.querySelector('.remove').addEventListener('click', function () {
          document.querySelector('.container').removeChild(wrapper);
        }, false);

        document.querySelector('.add').addEventListener('click', function () {
          wrapper.appendChild(document.createTextNode('\t' + ++counter + ':a new line text\n'));
        }, false);
      </script>
    </body>
</html>

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

因此应该做如下修改

// 因为要多次用到<pre>节点,将其缓存到本地变量wrapper中,
var wrapper = document.querySelector('.wrapper');
var counter = 0;

document.querySelector('.remove').addEventListener('click', function () {
  document.querySelector('.container').removeChild(wrapper);
  wrapper = null;//在执行删除操作时,将wrapper对pre节点的引用释放掉
}, false);

document.querySelector('.add').addEventListener('click', function () {
  wrapper.appendChild(document.createTextNode('\t' + ++counter + ':a new line text\n'));
}, false);