web前端 - 5个常见的JavaScript内存错误

30 篇文章 1 订阅

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 (

Loading ...

);

}

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 (

{showTimer ? (

): (

<button onClick={() => setShowTimer(true)}>

Retry

)}

)

}

当我们点击重试按钮时,可以看到分配的内存越来越多。这说明之前分配的内存没有被释放。计时器仍然在运行而不是被替换。

怎么解决这个问题?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 数组将使它们有资格进行垃圾收集。

前端技术每年都会不断更新,学习前端开发建议大家还是要选择培训为好,推荐爱创课堂,是一家专门做前端教育的学校,爱创课堂是一家以就业为导向的前端培训学校,所学习的就是企业所需要的,培养企业最需要的前端工程师为企业理念,授课以实战课程为主,更多的学习大项目对以后工作有帮助,毕业后轻松就业!

一位好的Web前端开发工程师在知识体系上既要有广度,又要有深度,所以很多大公司即使出高薪也很难招聘到理想的前端开发工程师。

那么如何系统的学习企业实用的web前端技术呢,视频教程、学习路线、

前端学习培训,可以加威haomei0452 获取前端学习资料。

相信从中会受到启发,找到学习的方向和目标。如大家对前端还有不了解的问题,可以持续关注我。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值