五个常见的JavaScript内存错误

时间:2022-06-01 20:34:29

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

五个常见的JavaScript内存错误

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

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

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

什么是内存泄漏?

 

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

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

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

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

计时器监听器

 

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

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

- MDN Web Docs

让我们创建一个调用回调函数的组件,以发出x循环后的完成。我正在为这个特定的例子做出反应,但这适用于任何FE框架。

  1. import React, { useRef } from 'react'; 
  2.  
  3. const Timer = ({ cicles, onFinish }) => { 
  4.     const currentCicles = useRef(0); 
  5.  
  6.     setInterval(() => { 
  7.         if (currentCicles.current >= cicles) { 
  8.             onFinish(); 
  9.             return; 
  10.         } 
  11.         currentCicles.current++; 
  12.     }, 500); 
  13.  
  14.     return ( 
  15.         <div>Loading ...</div> 
  16.     ); 
  17.  
  18. export default Timer; 

起初,看起来没有什么是错的。让我们创建一个触发此计时器的组件,并分析其内存性能:

  1. import React, { useState } from 'react'; 
  2. import styles from '../styles/Home.module.css' 
  3. import Timer from '../components/Timer'; 
  4.  
  5. export default function Home() { 
  6.     const [showTimer, setShowTimer] = useState(); 
  7.     const onFinish = () => setShowTimer(false); 
  8.  
  9.     return ( 
  10.       <div className={styles.container}> 
  11.           {showTimer ? ( 
  12.               <Timer cicles={10} onFinish={onFinish} /> 
  13.           ): ( 
  14.               <button onClick={() => setShowTimer(true)}> 
  15.                 Retry 
  16.               </button> 
  17.           )} 
  18.       </div> 
  19.     ) 

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

五个常见的JavaScript内存错误

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

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

  1. useEffect(() => { 
  2.     const intervalId = setInterval(() => { 
  3.         if (currentCicles.current >= cicles) { 
  4.             onFinish(); 
  5.             return; 
  6.         } 
  7.         currentCicles.current++; 
  8.     }, 500); 
  9.  
  10.     return () => clearInterval(intervalId); 
  11. }, []) 

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

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

  1. import { useEffect } from 'react'; 
  2.  
  3. export const useTimeout = (refreshCycle = 100, callback) => { 
  4.     useEffect(() => { 
  5.         if (refreshCycle <= 0) { 
  6.             setTimeout(callback, 0); 
  7.             return; 
  8.         } 
  9.  
  10.         const intervalId = setInterval(() => { 
  11.             callback(); 
  12.         }, refreshCycle); 
  13.  
  14.         return () => clearInterval(intervalId); 
  15.     }, [refreshCycle, setInterval, clearInterval]); 
  16. }; 
  17.  
  18. export default useTimeout; 

现在,无论何时需要使用SetInterval,您都可以执行以下操作:

  1. const handleTimeout = () => ...; 
  2. useTimeout(100, handleTimeout); 

现在,您可以使用此USETIMEOUT挂钩而无需担心内存泄露,它都是由抽象管理的。

2. 事件监听器

 

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

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

  1. function homeShortcuts({ key}) { 
  2.     if (key === 'E') { 
  3.         console.log('edit widget') 
  4.     } 
  5.  
  6. // user lands on home and we execute 
  7. document.addEventListener('keyup', homeShortcuts);  
  8.  
  9.  
  10. // user does some stuff and navigates to settings 
  11.  
  12. function settingsShortcuts({ key}) { 
  13.     if (key === 'E') { 
  14.         console.log('edit setting') 
  15.     } 
  16.  
  17. // user lands on home and we execute 
  18. document.addEventListener('keyup', settingsShortcuts);  

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

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

  1. document.removeEventListener(‘keyup’, homeShortcuts); 

让我们重构代码以防止这种不需要的行为:

  1. function homeShortcuts({ key}) { 
  2.     if (key === 'E') { 
  3.         console.log('edit widget') 
  4.     } 
  5.  
  6. // user lands on home and we execute 
  7. document.addEventListener('keyup', homeShortcuts);  
  8.  
  9.  
  10. // user does some stuff and navigates to settings 
  11.  
  12. function settingsShortcuts({ key}) { 
  13.     if (key === 'E') { 
  14.         console.log('edit setting') 
  15.     } 
  16.  
  17. // user lands on home and we execute 
  18. document.removeEventListener('keyup', homeShortcuts);  
  19. document.addEventListener('keyup', settingsShortcuts); 

作为拇指的规则,当使用来自全局对象的工具时,您需要谨慎且负责任。

3. 观察者

 

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

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

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

- MDN Web Docs

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

让我们看一些代码:

  1. const ref = ... 
  2. const visible = (visible) => { 
  3.   console.log(`It is ${visible}`); 
  4.  
  5. useEffect(() => { 
  6.     if (!ref) { 
  7.         return; 
  8.     } 
  9.  
  10.     observer.current = new IntersectionObserver( 
  11.         (entries) => { 
  12.             if (!entries[0].isIntersecting) { 
  13.                 visible(true); 
  14.             } else { 
  15.                 visbile(false); 
  16.             } 
  17.         }, 
  18.         { rootMargin: `-${header.height}px` }, 
  19.     ); 
  20.  
  21.     observer.current.observe(ref); 
  22. }, [ref]); 

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

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

4. 窗口对象

 

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

让我们来看看以下例子:

  1. function addElement(element) { 
  2.     if (!this.stack) { 
  3.         this.stack = { 
  4.             elements: [] 
  5.         } 
  6.     } 
  7.  
  8.     this.stack.elements.push(element); 

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

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

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

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

  1. "use strict" 

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

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

  • 在Addelement函数上,从全局范围内调用时,这将是未定义的。
  • 如果您未指定const |左撇子var在变量上,您将收到以下错误:
    1. Uncaught ReferenceError: b is not defined 

5. 持有DOM参考

 

DOM节点也没有内存泄漏。你需要小心不要抓住他们的参考。否则,垃圾收集器将无法清除它们,因为它们仍然可以到达。

让我们看一个小的代码示例来说明这个:

  1. const elements = []; 
  2. const list = document.getElementById('list'); 
  3.  
  4. function addElement() { 
  5.     // clean nodes 
  6.     list.innerHTML = ''
  7.  
  8.     const divElementdocument.createElement('div'); 
  9.     const element = document.createTextNode(`adding element ${elements.length}`); 
  10.     divElement.appendChild(element); 
  11.  
  12.  
  13.     list.appendChild(divElement); 
  14.     elements.push(divElement); 
  15.  
  16. document.getElementById('addElement').onclick = addElement

请注意,AddElement函数清除列表DIV并将新元素添加为子项。此新创建的元素将添加到元素数组中。

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

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

五个常见的JavaScript内存错误

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

结论

 

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

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

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

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

干杯!

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