乐趣区

关于前端:5个常见的JavaScript内存错误

本文首发于微信公众号:大迁世界, 我的微信:qq449245884,我会第一工夫和你分享前端行业趋势,学习路径等等。
更多开源作品请看 GitHub https://github.com/qq449245884/xiaozhi,蕴含一线大厂面试残缺考点、材料以及我的系列文章。

JavaScript 不提供任何内存治理操作。相同,内存由 JavaScript VM 通过内存回收过程治理,该过程称为 垃圾收集

既然咱们不能强制的垃圾回收,那咱们怎么晓得它能失常工作?咱们对它又理解多少呢?

  • 脚本执行在此过程中暂停
  • 它为不可拜访的资源开释内存
  • 它是不确定的
  • 它不会一次查看整个内存,而是在多个周期中运行
  • 它是不可预测的,但它会在必要时执行

这是否意味着无需放心资源和内存调配问题? 当然不是。如果咱们一不小心,可能会产生一些内存透露。

什么是内存透露?

内存透露是软件无奈回收的已调配的内存块。

Javascript 提供了一个垃圾收集程序,但这并不意味着咱们就能防止内存透露。为了合乎垃圾收集的条件,该对象必须不被其余中央援用。如果持有对未应用的资源的援用,这将会阻止这些资源被回收。这就是所谓的 有意识的内存放弃

泄露内存可能会导致垃圾收集器更频繁地运行。因为这个过程会阻止脚本的运行,它可能会让咱们程序卡起来,这么一卡,挑剔的用户必定会留神到,一用不爽了,那这个产品离下线的日子就不完了。更重大可能会让整个利用奔溃,那就 gg 了。

如何避免内存透露? 次要还是咱们应该防止保留不必要的资源。来看看一些常见的场景。

1. 计时器的监听

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;

一看,如同没啥问题。不急,咱们再创立一个触发这个定时器的组件,并剖析其内存性能。


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

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>
    )
}

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

当咱们点击重试按钮时,能够看到调配的内存越来越多。这阐明之前调配的内存没有被开释。计时器依然在运行而不是被替换。

怎么解决这个问题?setInterval 的返回值是一个距离 ID,咱们能够用它来勾销这个距离。在这种非凡状况下,咱们能够在组件卸载后调用 clearInterval

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

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

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

这里应用的是 React,咱们能够把所有这些逻辑都包装在一个自定义的 Hook 中。


import {useEffect} from 'react';

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;

当初须要应用 setInterval 时,都能够这样做:

const handleTimeout = () => ...;

useTimeout(100, handleTimeout);

当初你能够应用这个useTimeout Hook,而不用放心内存被泄露,这也是抽象化的益处。

2. 事件监听

Web API 提供了大量的事件监听器。在后面,咱们探讨了setTimeout。当初来看看 addEventListener

在这个事例中,咱们创立一个键盘快捷键性能。因为咱们在不同的页面上有不同的性能,所以将创立不同的快捷键性能

function homeShortcuts({key}) {if (key === 'E') {console.log('edit widget')
    }
}

// 用户在主页上登陆,咱们执行
document.addEventListener('keyup', homeShortcuts); 


// 用户做一些事件,而后导航到设置

function settingsShortcuts({key}) {if (key === 'E') {console.log('edit setting')
    }
}

// 用户在主页上登陆,咱们执行
document.addEventListener('keyup', settingsShortcuts); 

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

要革除之前的回调,咱们须要应用 removeEventListener :

document.removeEventListener(‘keyup’, homeShortcuts);

重构一下下面的代码:


function homeShortcuts({key}) {if (key === 'E') {console.log('edit widget')
    }
}

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


// user does some stuff and navigates to settings

function settingsShortcuts({key}) {if (key === 'E') {console.log('edit setting')
    }
}

// user lands on home and we execute
document.removeEventListener('keyup', homeShortcuts); 
document.addEventListener('keyup', settingsShortcuts);

依据教训,当应用来自全局对象的工具时,须要灰常小心。

3.Observers

Observers 是一个浏览器的 Web API 性能,很多开发者都不晓得。如果你想查看 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]);

下面的代码看起来不错。然而,一旦组件被卸载,观察者会产生什么? 它不会被革除,那内存可就透露了。咱们怎么解决这个问题呢? 只须要应用 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]);

4. Window Object

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

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

    this.stack.elements.push(element);
}

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

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

var a = 'example 1'; // 作用域限定在创立 var 的中央
b = 'example 2'; // 增加到 Window 对象中

要避免这种问题能够应用严格模式:

"use strict"

通过应用严格模式,向 JavaScript 编译器暗示,你想爱护本人免受这些行为的影响。当你须要时,你依然能够应用 Window。不过,你必须以明确的形式应用它。

严格模式是如何影响咱们后面的例子:

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

5. 持有 DOM 援用

DOM 节点也不能防止内存透露。咱们须要留神不要保留它们的援用。否则,垃圾回收器将无奈清理它们,因为它们依然是可拜访的。

用一小段代码演示一下:

const elements = [];
const list = document.getElementById('list');

function addElement() {
    // clean nodes
    list.innerHTML = '';

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


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

document.getElementById('addElement').onclick = addElement;

留神,addElement 函数革除列表 div,并将一个新元素作为子元素增加到它中。这个新创建的元素被增加到 elements 数组中。

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

咱们在执行几次之后监督函数:

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

总结

在这篇文章中,咱们曾经看到了最常见的内存泄露形式。很显著,JavaScript 自身并没有透露内存。相同,它是由开发者方面无心的内存放弃造成的。只有代码是整洁的,而且咱们不忘本人清理,就不会产生透露。

理解内存和垃圾回收在 JavaScript 中是如何工作的是必须的。一些开发者失去了谬误的意识,认为因为它是主动的,所以他们不须要放心这个问题。

~ 完,我是小智,励志退休后,回家摆地摊的码农。


编辑中可能存在的 bug 没法实时晓得,预先为了解决这些 bug, 花了大量的工夫进行 log 调试,这边顺便给大家举荐一个好用的 BUG 监控工具 Fundebug。

作者:Jose Granja 译者:前端小智 起源:medium

原文:https://betterprogramming.pub…

交换

有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

退出移动版