文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

五个常见的JavaScript内存错误

2024-12-02 11:27

关注

[[443138]]

既然我们不能强制的垃圾回收,那我们怎么知道它能正常工作?我们对它又了解多少呢?

这是否意味着无需担心资源和内存分配问题?当然不是。如果我们一不小心,可能会产生一些内存泄漏。

什么是内存泄漏?

内存泄漏是软件无法回收的已分配的内存块。

Javascript 提供了一个垃圾收集程序,但这并不意味着我们就能避免内存泄漏。为了符合垃圾收集的条件,该对象必须不被其他地方引用。如果持有对未使用的资源的引用,这将会阻止这些资源被回收。这就是所谓的无意识的内存保持。

泄露内存可能会导致垃圾收集器更频繁地运行。由于这个过程会阻止脚本的运行,它可能会让我们程序卡起来,这么一卡,挑剔的用户肯定会注意到,一用不爽了,那这个产品离下线的日子就不完了。更严重可能会让整个应用奔溃,那就gg了。

如何防止内存泄漏? 主要还是我们应该避免保留不必要的资源。来看看一些常见的场景。

1.计时器的监听

setInterval() 方法重复调用函数或执行代码片段,每次调用之间有固定的时间延迟。它返回一个时间间隔ID,该ID唯一地标识时间间隔,因此您可以稍后通过调用 clearInterval() 来删除它。

我们创建一个组件,它调用一个回调函数来表示它在x个循环之后完成了。我在这个例子中使用React,但这适用于任何FE框架。

 

  1. import React, { useRef } from 'react'
  2.  
  3. const Timer = ({ cicles, onFinish }) => { 
  4.     const currentCicles = useRef(0); 
  5.  
  6.     setInterval(() => { 
  7.         if (currentCicles.current >= cicles) { 
  8.             onFinish(); 
  9.             return
  10.         } 
  11.         currentCicles.current++; 
  12.     }, 500); 
  13.  
  14.     return ( 
  15.         
    Loading ...
     
  16.     ); 
  17.  
  18. export default Timer; 

 

一看,好像没啥问题。不急,我们再创建一个触发这个定时器的组件,并分析其内存性能。

  1. import React, { useState } from 'react'
  2. import styles from '../styles/Home.module.css' 
  3. import Timer from '../components/Timer'
  4.  
  5. export default function Home() { 
  6.     const [showTimer, setShowTimer] = useState(); 
  7.     const onFinish = () => setShowTimer(false); 
  8.  
  9.     return ( 
  10.        
  11.           {showTimer ? ( 
  12.                
  13.           ): ( 
  14.                setShowTimer(true)}> 
  15.                 Retry 
  16.                
  17.           )} 
  18.       
 
  •     ) 
  • 在 Retry 按钮上单击几次后,这是使用Chrome Dev Tools获取内存使用的结果:

     

     

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

    怎么解决这个问题?setInterval 的返回值是一个间隔 ID,我们可以用它来取消这个间隔。在这种特殊情况下,我们可以在组件卸载后调用 clearInterval。

     

    1. useEffect(() => { 
    2.     const intervalId = setInterval(() => { 
    3.         if (currentCicles.current >= cicles) { 
    4.             onFinish(); 
    5.             return
    6.         } 
    7.         currentCicles.current++; 
    8.     }, 500); 
    9.  
    10.     return () => clearInterval(intervalId); 
    11. }, []) 

     

    有时,在编写代码时,很难发现这个问题,最好的方式,还是要把组件抽象化。

    这里使用的是React,我们可以把所有这些逻辑都包装在一个自定义的 Hook 中。

     

    1. import { useEffect } from 'react'
    2.  
    3. export const useTimeout = (refreshCycle = 100, callback) => { 
    4.     useEffect(() => { 
    5.         if (refreshCycle <= 0) { 
    6.             setTimeout(callback, 0); 
    7.             return
    8.         } 
    9.  
    10.         const intervalId = setInterval(() => { 
    11.             callback(); 
    12.         }, refreshCycle); 
    13.  
    14.         return () => clearInterval(intervalId); 
    15.     }, [refreshCycle, setInterval, clearInterval]); 
    16. }; 
    17.  
    18. export default useTimeout; 

     

    现在需要使用setInterval时,都可以这样做:

     

    1. const handleTimeout = () => ...; 
    2.  
    3. useTimeout(100, handleTimeout); 

     

    现在你可以使用这个useTimeout Hook,而不必担心内存被泄露,这也是抽象化的好处。

    2.事件监听

    Web API提供了大量的事件监听器。在前面,我们讨论了setTimeout。现在来看看 addEventListener。

    在这个事例中,我们创建一个键盘快捷键功能。由于我们在不同的页面上有不同的功能,所以将创建不同的快捷键功能

     

    1. function homeShortcuts({ key}) { 
    2.     if (key === 'E') { 
    3.         console.log('edit widget'
    4.     } 
    5.  
    6. // 用户在主页上登陆,我们执行 
    7. document.addEventListener('keyup', homeShortcuts);  
    8.  
    9.  
    10. // 用户做一些事情,然后导航到设置 
    11.  
    12. function settingsShortcuts({ key}) { 
    13.     if (key === 'E') { 
    14.         console.log('edit setting'
    15.     } 
    16.  
    17. // 用户在主页上登陆,我们执行 
    18. document.addEventListener('keyup', settingsShortcuts);  

     

    看起来还是很好,除了在执行第二个 addEventListener 时没有清理之前的 keyup。这段代码不是替换我们的 keyup 监听器,而是将添加另一个 callback。这意味着,当一个键被按下时,它将触发两个函数。

    要清除之前的回调,我们需要使用 removeEventListener :

     

    1. document.removeEventListener(‘keyup’, homeShortcuts); 

    重构一下上面的代码:

     

    1. function homeShortcuts({ key}) { 
    2.     if (key === 'E') { 
    3.         console.log('edit widget'
    4.     } 
    5.  
    6. // user lands on home and we execute 
    7. document.addEventListener('keyup', homeShortcuts);  
    8.  
    9.  
    10. // user does some stuff and navigates to settings 
    11.  
    12. function settingsShortcuts({ key}) { 
    13.     if (key === 'E') { 
    14.         console.log('edit setting'
    15.     } 
    16.  
    17. // user lands on home and we execute 
    18. document.removeEventListener('keyup', homeShortcuts);  
    19. document.addEventListener('keyup', settingsShortcuts); 

     

    根据经验,当使用来自全局对象的工具时,需要灰常小心。

    3.Observers

    Observers 是一个浏览器的 Web API功能,很多开发者都不知道。如果你想检查HTML元素的可见性或大小的变化,这个就很强大了。

    IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

    尽管它很强大,但我们也要谨慎的使用它。一旦完成了对对象的观察,就要记得在不用的时候取消它。

    看看代码:

     

    1. const ref = ... 
    2. const visible = (visible) => { 
    3.   console.log(`It is ${visible}`); 
    4.  
    5. useEffect(() => { 
    6.     if (!ref) { 
    7.         return
    8.     } 
    9.  
    10.     observer.current = new IntersectionObserver( 
    11.         (entries) => { 
    12.             if (!entries[0].isIntersecting) { 
    13.                 visible(true); 
    14.             } else { 
    15.                 visbile(false); 
    16.             } 
    17.         }, 
    18.         { rootMargin: `-${header.height}px` }, 
    19.     ); 
    20.  
    21.     observer.current.observe(ref); 
    22. }, [ref]); 

     

    上面的代码看起来不错。然而,一旦组件被卸载,观察者会发生什么?它不会被清除,那内存可就泄漏了。我们怎么解决这个问题呢?只需要使用 disconnect 方法:

     

    1. const ref = ... 
    2. const visible = (visible) => { 
    3.   console.log(`It is ${visible}`); 
    4.  
    5. useEffect(() => { 
    6.     if (!ref) { 
    7.         return
    8.     } 
    9.  
    10.     observer.current = new IntersectionObserver( 
    11.         (entries) => { 
    12.             if (!entries[0].isIntersecting) { 
    13.                 visible(true); 
    14.             } else { 
    15.                 visbile(false); 
    16.             } 
    17.         }, 
    18.         { rootMargin: `-${header.height}px` }, 
    19.     ); 
    20.  
    21.     observer.current.observe(ref); 
    22.  
    23.     return () => observer.current?.disconnect(); 
    24. }, [ref]); 

     

    4. Window Object

    向 Window 添加对象是一个常见的错误。在某些场景中,可能很难找到它,特别是在使用 Window Execution上下文中的this关键字。看看下面的例子:

     

    1. function addElement(element) { 
    2.     if (!this.stack) { 
    3.         this.stack = { 
    4.             elements: [] 
    5.         } 
    6.     } 
    7.  
    8.     this.stack.elements.push(element); 

     

    它看起来无害,但这取决于你从哪个上下文调用addElement。如果你从Window Context调用addElement,那就会越堆越多。

    另一个问题可能是错误地定义了一个全局变量:

     

    1. var a = 'example 1'; // 作用域限定在创建var的地方 
    2. b = 'example 2'; // 添加到Window对象中 

     

    要防止这种问题可以使用严格模式:

     

    1. "use strict" 

    通过使用严格模式,向JavaScript编译器暗示,你想保护自己免受这些行为的影响。当你需要时,你仍然可以使用Window。不过,你必须以明确的方式使用它。

    严格模式是如何影响我们前面的例子:

    1. Uncaught ReferenceError: b is not defined 

    5. 持有DOM引用

    DOM节点也不能避免内存泄漏。我们需要注意不要保存它们的引用。否则,垃圾回收器将无法清理它们,因为它们仍然是可访问的。

    用一小段代码演示一下:

     

    1. const elements = []; 
    2. const list = document.getElementById('list'); 
    3.  
    4. function addElement() { 
    5.     // clean nodes 
    6.     list.innerHTML = ''
    7.  
    8.     const divElement= document.createElement('div'); 
    9.     const element = document.createTextNode(`adding element ${elements.length}`); 
    10.     divElement.appendChild(element); 
    11.  
    12.  
    13.     list.appendChild(divElement); 
    14.     elements.push(divElement); 
    15.  
    16. document.getElementById('addElement').onclick = addElement; 

     

    注意,addElement 函数清除列表 div,并将一个新元素作为子元素添加到它中。这个新创建的元素被添加到 elements 数组中。

    下一次执行 addElement 时,该元素将从列表 div 中删除,但是它不适合进行垃圾收集,因为它存储在 elements 数组中。

    我们在执行几次之后监视函数:

     

     

    在上面的截图中看到节点是如何被泄露的。那怎么解决这个问题?清除 elements 数组将使它们有资格进行垃圾收集。

    总结

    在这篇文章中,我们已经看到了最常见的内存泄露方式。很明显,JavaScript本身并没有泄漏内存。相反,它是由开发者方面无意的内存保持造成的。只要代码是整洁的,而且我们不忘自己清理,就不会发生泄漏。

    了解内存和垃圾回收在JavaScript中是如何工作的是必须的。一些开发者得到了错误的意识,认为由于它是自动的,所以他们不需要担心这个问题。

     

     

    来源:今日头条内容投诉

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    软考中级精品资料免费领

    • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

      难度     813人已做
      查看
    • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

      难度     354人已做
      查看
    • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

      难度     318人已做
      查看
    • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

      难度     435人已做
      查看
    • 2024年上半年系统架构设计师考试综合知识真题

      难度     224人已做
      查看

    相关文章

    发现更多好内容

    猜你喜欢

    AI推送时光机
    位置:首页-资讯-后端开发
    咦!没有更多了?去看看其它编程学习网 内容吧
    首页课程
    资料下载
    问答资讯