#yyds干货盘点#常见的JavaScript内存错误
Posted 尼羲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#yyds干货盘点#常见的JavaScript内存错误相关的知识,希望对你有一定的参考价值。
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.heightpx` ,
);
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.heightpx` ,
);
observer.current.observe(ref);
return ()
以上是关于#yyds干货盘点#常见的JavaScript内存错误的主要内容,如果未能解决你的问题,请参考以下文章
# yyds干货盘点 # 一文带你解读JavaScript中的变量作用域和内存问题
#yyds干货盘点# Java 内存分析之堆内存和MetaSpace内存