js常见内存泄漏及避免方法

144 阅读3分钟

在js中我们声明的变量会占有一定的内存空间,正常情况下这些变量在代码执行完之后就会被gc回收,所占用的内存也会被释放,当某个变量不再被使用但是却没有被回收就会造成内存泄漏。

一、 全局变量

window上声明的变量,尤其是数据量很大的变量在不需要的时候要及时清除,否则会一直存在。

二、 dom引用

虽然删除了dom,但是由于被引用导致cotainer仍然在内存中无法被释放。这种情况要及时清除dom的引用:

 const container = document.getElementById('container');
 if(container){
    container.parentNode.removeChild(container); 
    container = null; // 清除dom引用
 }

三、闭包

闭包本身不会导致内存泄漏,当闭包使用完后没有清除引用,就会导致内存泄漏。

  function foo(){
       let count = 0;
       return function(){
          return count++;
       }
  }
  
  const getCount = foo();
  console.log(getCount()); // 0
  // 后续不再使用了
  getCount = null; // 要及时引用清除

四、setInterval

setInterval在使用完之后要及时清除,除此之有些场景要在执行到一定时间或一定次数之后清除掉定时器,否则会一直执行,使回调函数里面的引用无法释放,造成内存泄漏。

    let count = 0;
    this.timer = setInterval(()=>{
      const container = document.getElementById("container");
      if (container) {
         container.innerHTML = "hello world";
         clearInterval(this.timer);
      }else if(count === 20){ // 2秒后清除定时器
        clearInterval(this.timer);
      }
      count++;
    },100);

五、setTimeout

如果回调函数中引入了外部的变量,直到回调函数执行完之前,这些变量会一直被引用无法被gc回收。最常见的就是react中使用了this,组件卸载的时候this仍然被引用,导致内存泄漏。这种情况要在组件卸载之前将this设置为null。

image.png

setTimeout在重复调用的时候(遍历、递归、组件声明周期等)每次调用之前要先清除之前的定时器,否则会同时存在多个定时器造成内存泄露

    typeWords = (words, container) => {
        let index = 0;
        let timer;
        const type = (text) => {
          if (index< text.length) {
            clearTimeout(timer); // 声明新的定时器之前先清除掉旧的
            timer = setTimeout(() => {
              container.innerHTML += words.charAt(index);
              index++;
              type(text);
            }, 100);
          }
        };
        if (container) type(words);
      };

在react的周期函数中使用定时器要格外小心,尤其是在didUpdate的时候,每次组件更新都会产生一个新的setTimeout而componentWillUnmount中只能清除最后一个,这种情况要在声明新的setTimeout之后清除掉旧的。

  componentDidUpdate(){
    clearTimeout(this.timer); //清除定时器,
    this.timer = setTimeout(() => {
        console.log('componentDidUpdate')
    }, 200);
  }

  componentWillUnmount() {
    clearTimeout(this.timer); //清除定时器,
  }

六、 ReactDom.render

使用ReactDom.render渲染组件的时候,当组件卸载的时候并不会卸载组件,组件在内存中依然存在,要调用ReactDom.unmountComponentAtNode(container)方法手动卸载组件。

setTimeout(() => {
    ReactDOM.render(<App />, document.getElementById("root"));
}, 1000);

七、 事件监听

事件注册分为三种:dom事件、全局事件、自定义事件。不管是哪种事件在使用完之后都要取消掉。

 componentDidMount() {
    const container = document.getElementById("container");
    if (container) {
        container.addEventListener('click', this.click, true);
    }
    
    window.addEventListener('resize', this.resize);
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.resize);
    conatiner.removeEventListener('click', this.click, true);
  }

需要注意的是在取消事件监听的时候,事件类型、事件、参数要完全一致。 一些三方库的事件机制中会自动帮我们取消事件监听如EventEmitter中的once和react中的事件等。