JS中如何防止内存泄漏?聊聊5个常见内存错误
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中是如何工作的是必须的。一些开发者得到了错误的意识,认为由于它是自动的,所以他们不需要担心这个问题。
原文地址:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2
作者: Jose Granja
译者:前端小智
【相关推荐:javascript学习教程】
以上是JS中如何防止内存泄漏?聊聊5个常见内存错误的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

热门话题

如何使用WebSocket和JavaScript实现在线语音识别系统引言:随着科技的不断发展,语音识别技术已经成为了人工智能领域的重要组成部分。而基于WebSocket和JavaScript实现的在线语音识别系统,具备了低延迟、实时性和跨平台的特点,成为了一种被广泛应用的解决方案。本文将介绍如何使用WebSocket和JavaScript来实现在线语音识别系

相信很多小伙伴使用电脑的时候,突然弹出一个对话框:该内存不能为read并闪退,让人一脸懵逼,这是什么情况呢?其实这是因为驱动程序不稳定或者没有更新,引发操作系统与计算机硬件不兼容,容易导致内存不能为read错误,可以通过软件工具进行一键修复。该内存不能为read怎么办:第一种:修复工具1、下载一个专用的修复工具。2、下载好之后将它解压出来并运行“一键修复内存不能为read”即可解决。第二种:手工修复1、首先按下快捷键windows+R,打开运行后输入CMD并回车。2、打开命令提示符后,在其中输入

WebSocket与JavaScript:实现实时监控系统的关键技术引言:随着互联网技术的快速发展,实时监控系统在各个领域中得到了广泛的应用。而实现实时监控的关键技术之一就是WebSocket与JavaScript的结合使用。本文将介绍WebSocket与JavaScript在实时监控系统中的应用,并给出代码示例,详细解释其实现原理。一、WebSocket技

如何利用JavaScript和WebSocket实现实时在线点餐系统介绍:随着互联网的普及和技术的进步,越来越多的餐厅开始提供在线点餐服务。为了实现实时在线点餐系统,我们可以利用JavaScript和WebSocket技术。WebSocket是一种基于TCP协议的全双工通信协议,可以实现客户端与服务器的实时双向通信。在实时在线点餐系统中,当用户选择菜品并下单

如何使用WebSocket和JavaScript实现在线预约系统在当今数字化的时代,越来越多的业务和服务都需要提供在线预约功能。而实现一个高效、实时的在线预约系统是至关重要的。本文将介绍如何使用WebSocket和JavaScript来实现一个在线预约系统,并提供具体的代码示例。一、什么是WebSocketWebSocket是一种在单个TCP连接上进行全双工

JavaScript和WebSocket:打造高效的实时天气预报系统引言:如今,天气预报的准确性对于日常生活以及决策制定具有重要意义。随着技术的发展,我们可以通过实时获取天气数据来提供更准确可靠的天气预报。在本文中,我们将学习如何使用JavaScript和WebSocket技术,来构建一个高效的实时天气预报系统。本文将通过具体的代码示例来展示实现的过程。We

JavaScript教程:如何获取HTTP状态码,需要具体代码示例前言:在Web开发中,经常会涉及到与服务器进行数据交互的场景。在与服务器进行通信时,我们经常需要获取返回的HTTP状态码来判断操作是否成功,根据不同的状态码来进行相应的处理。本篇文章将教你如何使用JavaScript获取HTTP状态码,并提供一些实用的代码示例。使用XMLHttpRequest

用法:在JavaScript中,insertBefore()方法用于在DOM树中插入一个新的节点。这个方法需要两个参数:要插入的新节点和参考节点(即新节点将要被插入的位置的节点)。
