优秀的编程知识分享平台

网站首页 > 技术文章 正文

五个常见的JavaScript内存错误(js导致内存泄露的几种问题)

nanyue 2024-07-26 15:40:43 技术文章 8 ℃

提示以避免内存在Web应用程序中泄漏

JavaScript没有提供任何内存管理原语。相反,内存由JavaScript VM通过内存回收过程管理。该过程称为垃圾收集。

由于我们不能强迫它运行,我们如何知道它会正常工作?我们对此了解了什么?

  • 脚本执行在此过程中暂停。
  • 它释放内存以实现无法访问的资源。
  • 这是非确定性的。
  • 它不会一次性检查整个内存,但将在多个周期中运行。
  • 这是不可预测的。它将在必要时执行。

这是否意味着我们不必担心资源和内存分配?当然不是。如果您不小心,您可能会创建一些内存泄漏。

什么是内存泄漏?

内存泄漏是软件无法回收的分配的存储器。

javascript为您提供垃圾收集过程并不意味着您可以从内存泄漏中安全。为了有资格获得垃圾收集,必须在其他地方引用对象。如果您持有对未使用的资源的引用,则会阻止这些资源未分配。这被称为无意的记忆保留。

泄漏内存可能导致更频繁的垃圾收集器运行。由于此过程将阻止脚本运行,因此可能会减慢您的Web应用程序。这将使您的表现较少,这将由用户注意到。它甚至可以导致您的Web应用程序崩溃。

我们如何防止我们的Web应用程序泄漏内存?这很简单:通过避免保留不必要的资源。让我们看看可能发生的最常见的场景。

计时器监听器

让我们来看看SetInterval定时器。它是一个常用的Web API功能。

“窗口和工作接口提供的setInterval()方法,重复调用函数或执行代码片段,每个呼叫之间的固定时间延迟。它返回唯一标识间隔的间隔ID,因此您可以通过调用ClearInterval()稍后删除它。该方法由WindoworWorkerglobalscope Mixin定义。“ - MDN Web Docs

让我们创建一个调用回调函数的组件,以发出x循环后的完成。我正在为这个特定的例子做出反应,但这适用于任何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>
    )
}

在重试按钮上单击几次后,这是我们使用Chrome Dev Tools获得内存使用的结果:

您可以看到在击中重试按钮时分配了越来越多的内存。这意味着分配的先前内存并没有释放。间隔计时器仍在运行而不是被替换。

我们如何解决这个问题?setInterval的返回是我们可以使用的间隔ID来取消间隔。在这个特定的方案中,我们可以在组件上卸载一旦组件才能调用ClearInterval。

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

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

有时,在代码审查中发现这些问题很难。最好的做法是创建抽象,您可以管理所有复杂性。

正如我们在此使用的反应,我们可以在自定义挂钩中包装所有这些逻辑:

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挂钩而无需担心内存泄露,它都是由抽象管理的。

2.事件监听器

Web API提供了大量的事件侦听器,您可以自己挂钩。以前,我们覆盖了settimout。现在我们将看addeventlistener。

让我们为我们的Web应用程序创建一个键盘快捷功能。由于我们在不同页面上有不同的功能,因此我们将创建不同的快捷函数:

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.addEventListener('keyup', settingsShortcuts); 

一切似乎很好,除了我们在执行第二个AddeventListener时没有清洁先前的键。此代码而不是更换我们的keyup侦听器,而不是更换keyup侦听器。这意味着当按下键时,它将触发两个功能。

要清除以前的回调,我们需要使用remove eventListener。让我们看看代码示例:

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.观察者

观察者是大量开发人员未知的浏览器Web API功能。如果您想检查HTML元素的可见性或大小的更改,它们是强大的。

让我们检查交叉点观察者API:

“Intersection Observer API提供了一种异步地观察目标元素与祖先元素或顶级文档的视口的交叉点的变化。” - MDN Web Docs

尽可能强大,您需要负责任地使用它。完成观察对象后,您需要取消监视过程。

让我们看一些代码:

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]);

上面的代码看起来很好。但是,一旦组件未安装,观察者会发生什么?它不会被清除,所以你会泄漏内存。我们怎样才能解决这个问题?只需使用断开连接方法:

现在我们可以确定,当组件卸载时,我们的观察者将被断开连接。

4.窗口对象

将对象添加到窗口是一个常见的错误。在某些情况下,可能很难找到 - 特别是如果您使用窗口执行上下文中的此关键字。

让我们来看看以下例子:

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

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

它看起来无害,但这取决于你调用一个addelement的上下文。如果从窗口上下文中调用AddElement,则会开始查看堆积的项目。

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

var a = 'example 1'; // scoped to the place where var was createdb = 'example 2'; // added to the Window object

为防止这种问题,始终以严格模式执行JavaScript:

"use strict"

通过使用严格模式,您将暗示您想要保护自己免受这些类型的行为保护的JavaScript编译器。当您需要时,您仍然可以使用窗口。但是,您必须以明确的方式使用它。

如何影响我们之前的示例的严格模式:

  • 在Addelement函数上,从全局范围内调用时,这将是未定义的。
  • 如果您未指定const |左撇子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并将新元素添加为子项。此新创建的元素将添加到元素数组中。

下次执行AddElement,将从列表Div中删除该元素。但是,它不会有资格获得垃圾收集,因为它存储在元素数组中。这使得它可以到达。这将使您在每个addelement执行上的节点。

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

我们可以在上面的屏幕截图中看到节点如何泄露。我们怎样才能解决这个问题?清除元素数组将使它们有资格获得垃圾收集。

结论

在本文中,我们已经看到了最常见的方法可以泄露。很明显,JavaScript不会泄漏内存本身。相反,它是由从开发人员侧的无意的记忆保留引起的。只要代码整洁,我们就不会忘记在自己之后清理,不会发生泄漏。

了解JavaScript中的内存和垃圾收集工作是必须的。一些开发人员获得虚假印象,因为它是自动的,他们不需要担心它。

建议在Web应用程序上定期运行浏览器分析器工具。这是唯一能够肯定没有泄漏并留下的方法。Chrome开发人员性能选项卡是开始检测某些异常的地点。浏览问题后,您可以通过拍摄快照并进行比较,使用Profiler选项卡深入挖掘它。

有时,我们花费时间优化方法,忘记内存在我们的Web应用程序的性能中播放了一个很大的部分。

干杯!

(本文由闻数起舞翻译自undefined的文章《5 Common JavaScript Memory Mistakes》,转载请注明出处,原文链接:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2)

最近发表
标签列表