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应用程序的性能中播放了一个很大的部分。
干杯!
原文链接:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2