5가지 일반적인 JavaScript 메모리 오류

青灯夜游
풀어 주다: 2022-08-25 10:28:56
앞으로
1945명이 탐색했습니다.

5가지 일반적인 JavaScript 메모리 오류

JavaScript는 메모리 관리 작업을 제공하지 않습니다. 대신 가비지 수집이라는 메모리 회수 프로세스를 통해 JavaScript VM에서 메모리를 관리합니다. [관련 권장사항: javascript 학습 튜토리얼]

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

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

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

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

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

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 (
        <p>Loading ...</p>
    );
}

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 (
      <p>
          {showTimer ? (
              <timer></timer>
          ): (
              <button> setShowTimer(true)}>
                Retry
              </button>
          )}
      </p>
    )
}
로그인 후 복사

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

5가지 일반적인 JavaScript 메모리 오류

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

怎么解决这个问题?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 개발자 도구를 사용하여 메모리 사용량을 확인한 결과는 다음과 같습니다.

재시도를 클릭하면 버튼을 누르면 점점 더 많은 메모리가 할당되는 것을 확인할 수 있습니다. 이는 이전에 할당된 메모리가 해제되지 않았음을 의미합니다. 타이머는 교체되지 않고 계속 작동 중입니다.

이 문제를 해결하는 방법은 무엇입니까? 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('list');

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

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


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

document.getElementById('addElement').onclick = addElement;
로그인 후 복사

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

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

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

5가지 일반적인 JavaScript 메모리 오류

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

总结

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

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

作者: Jose Granja 

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

(学习视频分享:web前端

위 내용은 5가지 일반적인 JavaScript 메모리 오류의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:segmentfault.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!