>  기사  >  웹 프론트엔드  >  JS에서 메모리 누수를 방지하는 방법은 무엇입니까? 5가지 일반적인 메모리 오류에 대해 이야기해 보겠습니다.

JS에서 메모리 누수를 방지하는 방법은 무엇입니까? 5가지 일반적인 메모리 오류에 대해 이야기해 보겠습니다.

青灯夜游
青灯夜游앞으로
2021-12-31 10:57:281898검색

JS에서 메모리 누수를 방지하는 방법은 무엇입니까? 5가지 일반적인 메모리 오류에 대해 이야기해 보겠습니다.

JavaScript는 메모리 관리 작업을 제공하지 않습니다. 대신 가비지 수집이라는 메모리 회수 프로세스를 통해 JavaScript VM에서 메모리를 관리합니다.

가비지 수집을 강제할 수는 없는데 제대로 작동하는지 어떻게 알 수 있나요? 우리는 그것에 대해 얼마나 알고 있나요?

  • 이 프로세스 중에는 스크립트 실행이 일시 중지됩니다.
  • 액세스할 수 없는 리소스에 대해 메모리를 해제합니다.
  • 비결정적입니다.
  • 전체 메모리를 한 번에 확인하지 않고 여러 주기로 실행합니다.
  • 비결정적입니다 예측적입니다. , 하지만 필요할 때 실행됩니다.

이는 리소스 및 메모리 할당 문제에 대해 걱정할 필요가 없다는 것을 의미합니까? 주의하지 않으면 일부 메모리 누수가 발생할 수 있습니다.

JS에서 메모리 누수를 방지하는 방법은 무엇입니까? 5가지 일반적인 메모리 오류에 대해 이야기해 보겠습니다.

메모리 누수란 무엇인가요?

메모리 누수는 소프트웨어가 회수할 수 없는 할당된 메모리 블록입니다.

Javascript는 가비지 수집기를 제공하지만 이것이 메모리 누수를 피할 수 있다는 의미는 아닙니다. 가비지 수집 대상이 되려면 개체를 다른 곳에서 참조하면 안 됩니다. 사용되지 않은 리소스에 대한 참조를 보유하면 해당 리소스가 회수되지 않습니다. 이것을 무의식적 기억 유지라고 합니다.

메모리 누수로 인해 가비지 수집기가 더 자주 실행될 수 있습니다. 이 프로세스로 인해 스크립트가 실행되지 않으므로 프로그램이 정지될 수 있습니다. 이러한 지연이 발생하면 까다로운 사용자는 만족하지 않으면 제품이 오랫동안 오프라인 상태가 될 것임을 분명히 알 수 있습니다. 더 심각한 것은 전체 애플리케이션이 충돌할 수 있다는 것입니다.

메모리 누수를 방지하는 방법 가장 중요한 것은 불필요한 리소스를 유지하지 않는 것입니다. 몇 가지 일반적인 시나리오를 살펴보겠습니다.

1. 타이머 모니터링

setInterval() 메서드는 각 호출 사이에 고정된 시간 지연을 두고 반복적으로 함수를 호출하거나 코드 조각을 실행합니다. 간격을 고유하게 식별하는 간격 ID를 반환하므로 나중에 clearInterval()을 호출하여 삭제할 수 있습니다. 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 &#39;react&#39;;
import styles from &#39;../styles/Home.module.css&#39;
import Timer from &#39;../components/Timer&#39;;

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获取内存使用的结果:

JS에서 메모리 누수를 방지하는 방법은 무엇입니까? 5가지 일반적인 메모리 오류에 대해 이야기해 보겠습니다.

当我们点击重试按钮时,可以看到分配的内存越来越多。这说明之前分配的内存没有被释放。计时器仍然在运行而不是被替换。

怎么解决这个问题?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  {
            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

우리는 콜백 함수를 호출하여 x 루프 후에 완료되었음을 나타내는 구성 요소를 만듭니다. 이 예에서는 React를 사용하고 있지만 이는 모든 FE 프레임워크에서 작동합니다.

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);
다시 시도 버튼을 몇 번 클릭한 후 Chrome 개발자 도구를 사용하여 메모리 사용량을 가져온 결과는 다음과 같습니다.

JS에서 메모리 누수를 방지하는 방법은 무엇입니까? 5가지 일반적인 메모리 오류에 대해 이야기해 보겠습니다.재시도 버튼을 클릭하면 점점 더 많은 메모리가 할당되는 것을 볼 수 있습니다 . 이는 이전에 할당된 메모리가 해제되지 않았음을 의미합니다. 타이머는 교체되지 않고 계속 작동 중입니다.

이 문제를 해결하는 방법은 무엇입니까? setInterval의 반환 값은 간격을 취소하는 데 사용할 수 있는 간격 ID입니다. 이 특별한 경우에는 구성요소가 언로드된 후 clearInterval을 호출할 수 있습니다.

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]);
때로는 코드를 작성할 때 이 문제를 찾기 어려울 때가 있습니다. 가장 좋은 방법은 구성 요소를 추상화하는 것입니다. 여기에서는 React가 사용되며 이 모든 로직을 사용자 정의 Hook로 래핑할 수 있습니다. 🎜
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]);
🎜이제 setInterval을 사용해야 할 때마다 다음을 수행할 수 있습니다. 🎜
function addElement(element) {
    if (!this.stack) {
        this.stack = {
            elements: []
        }
    }

    this.stack.elements.push(element);
}
🎜이제 메모리 누수에 대한 걱정 없이 이 useTimeout Hook을 사용할 수 있습니다. 추상화 이점. 🎜🎜🎜🎜2. 이벤트 청취🎜🎜🎜🎜Web API는 다수의 이벤트 리스너를 제공합니다. 앞서 setTimeout에 대해 논의했습니다. 이제 addEventListener를 살펴보겠습니다. 🎜🎜이 예에서는 키보드 단축키 기능을 만듭니다. 페이지마다 다른 기능이 있으므로 다른 단축키 기능이 생성됩니다 🎜
var a = 'example 1'; // 作用域限定在创建var的地方
b = 'example 2'; // 添加到Window对象中
🎜 두 번째 addEventListener >keyup를 실행할 때 이전 . <code>keyup 리스너를 바꾸는 대신 이 코드는 또 다른 콜백을 추가합니다. 즉, 키를 누르면 두 가지 기능이 실행됩니다. 🎜🎜이전 콜백을 지우려면 removeEventListener를 사용해야 합니다. 🎜
"use strict"
🎜위 코드를 리팩토링하세요. 🎜
Uncaught ReferenceError: b is not defined
🎜경험에 따르면 전역 개체의 도구를 사용할 때는 매우 조심해야 합니다. 🎜🎜🎜🎜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(&#39;list&#39;);

function addElement() {
    // clean nodes
    list.innerHTML = &#39;&#39;;

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


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

document.getElementById(&#39;addElement&#39;).onclick = addElement;

注意,addElement 函数清除列表 div,并将一个新元素作为子元素添加到它中。这个新创建的元素被添加到 elements 数组中。

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

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

JS에서 메모리 누수를 방지하는 방법은 무엇입니까? 5가지 일반적인 메모리 오류에 대해 이야기해 보겠습니다.

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

总结

在这篇文章中,我们已经看到了最常见的内存泄露方式。很明显,JavaScript本身并没有泄漏内存。相反,它是由开发者方面无意的内存保持造成的。只要代码是整洁的,而且我们不忘自己清理,就不会发生泄漏。

了解内存和垃圾回收在JavaScript中是如何工作的是必须的。一些开发者得到了错误的意识,认为由于它是自动的,所以他们不需要担心这个问题。

原文地址:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2

作者: Jose Granja

译者:前端小智

【相关推荐:javascript学习教程

위 내용은 JS에서 메모리 누수를 방지하는 방법은 무엇입니까? 5가지 일반적인 메모리 오류에 대해 이야기해 보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제