Home>Article>Web Front-end> 5 common JavaScript memory errors
JavaScript does not provide any memory management operations. Instead, memory is managed by the JavaScript VM through a memory reclamation process called garbage collection. [Related recommendations:javascript learning tutorial]Since we can’t force garbage collection, how do we know it can work properly? How much do we know about it?
Script execution is paused during this processJavascript provides a garbage collector, but this does not mean that we can avoid memory leaks. To be eligible for garbage collection, the object must not be referenced elsewhere. If you hold references to unused resources, this will prevent those resources from being reclaimed. This is called
unconscious memory retention.Leaking memory may cause the garbage collector to run more frequently. Since this process will prevent the script from running, it may cause our program to freeze. If such a lag occurs, picky users will definitely notice that if they are not happy with it, the product will be offline for a long time. More seriously, it may cause the entire application to crash, which is gg.
How to prevent memory leaks? The main thing is that we should avoid retaining unnecessary resources. Let’s look at some common scenarios.
1. Timer monitoringmethod repeatedly calls a function or executes a code fragment, with a fixed interval between each call time delay. It returns an intervalID
that uniquely identifies the interval so you can later delete it by callingclearInterval()
.We create a component that calls a callback function to indicate that it is complete after
x
loops. I'm using React in this example, but this works with any FE framework.
import React, { useRef } from 'react'; const Timer = ({ cicles, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); return; } currentCicles.current++; }, 500); return (Loading ...
); } export default Timer;
At first glance, there seems to be no problem. Don't worry, let's create another component that triggers this timer and analyze its memory performance.
After a few clicks on theimport 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> ) }
button, this is the result of using Chrome Dev Tools to get the memory usage:
how to solve this problem? The return value of
setIntervalis an interval ID, which we can use to cancel this interval. In this particular case, we can call
clearIntervalafter the component is unloaded.
Sometimes, it is difficult to find this problem when writing code. The best way is to abstract the components.useEffect(() => { const intervalId = setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); return; } currentCicles.current++; }, 500); return () => clearInterval(intervalId); }, [])
React is used here, and we can wrap all this logic in a custom 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;
Now when you need to use
setInterval, you can do this:
const handleTimeout = () => ...; useTimeout(100, handleTimeout);
Now you can use thisuseTimeout Hook
without having to worry about the memory being Leak, this is also the benefit of abstraction.
2. Event listening
.In this case, we create a keyboard shortcut function. Since we have different functions on different pages, different shortcut key functions will be created
still looks fine except there is no cleanup before when executing the secondfunction 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);
The
keyup. Rather than replacing ourkeyup
listener, this code will add anothercallback
. This means that when a key is pressed, it triggers two functions.To clear the previous callback, we need to use
removeEventListener
:
document.removeEventListener(‘keyup’, homeShortcuts);
Refactor the above code:
According to experience, when using from Be extremely careful when working with global object tools.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);
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]);
向 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
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
数组中。
我们在执行几次之后监视函数:
在上面的截图中看到节点是如何被泄露的。那怎么解决这个问题?清除elements
数组将使它们有资格进行垃圾收集。
在这篇文章中,我们已经看到了最常见的内存泄露方式。很明显,JavaScript本身并没有泄漏内存。相反,它是由开发者方面无意的内存保持造成的。只要代码是整洁的,而且我们不忘自己清理,就不会发生泄漏。
了解内存和垃圾回收在JavaScript中是如何工作的是必须的。一些开发者得到了错误的意识,认为由于它是自动的,所以他们不需要担心这个问题。
作者: Jose Granja
原文:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2
(学习视频分享:web前端)
The above is the detailed content of 5 common JavaScript memory errors. For more information, please follow other related articles on the PHP Chinese website!