经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JavaScript » 查看文章
常见的JavaScript内存错误及解决方法
来源:jb51  时间:2021/12/31 10:40:59  对本文有异议

前言:

JavaScript 不提供任何内存管理操作。相反,内存由 JavaScript VM 通过内存回收过程管理,该过程称为垃圾收集。

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

脚本执行在此过程中暂停
它为不可访问的资源释放内存
它是不确定的
它不会一次检查整个内存,而是在多个周期中运行
它是不可预测的,但它会在必要时执行
这是否意味着无需担心资源和内存分配问题?当然不是。如果我们一不小心,可能会产生一些内存泄漏。

什么是内存泄漏?

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

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

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

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

1.计时器的监听

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

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

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

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

  1. import React, { useState } from 'react';
  2. import styles from '../styles/Home.module.css'
  3. import Timer from '../components/Timer';
  4. export default function Home() {
  5. const [showTimer, setShowTimer] = useState();
  6. const onFinish = () => setShowTimer(false);
  7. return (
  8. <div className={styles.container}>
  9. {showTimer ? (
  10. <Timer cicles={10} onFinish={onFinish} />
  11. ): (
  12. <button onClick={() => setShowTimer(true)}>
  13. Retry
  14. </button>
  15. )}
  16. </div>
  17. )
  18. }
  19.  
  20.  

在 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. return () => clearInterval(intervalId);
  10. }, [])
  11.  
  12.  

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

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

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

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

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

现在你可以使用这个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. function settingsShortcuts({ key}) {
  10. if (key === 'E') {
  11. console.log('edit setting')
  12. }
  13. }
  14. // 用户在主页上登陆,我们执行
  15. document.addEventListener('keyup', settingsShortcuts);
  16.  
  17.  

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

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

  1. document.removeEventListener(‘keyup', homeShortcuts);
  2.  
  3.  

重构一下上面的代码:

  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. // user does some stuff and navigates to settings
  9. function settingsShortcuts({ key}) {
  10. if (key === 'E') {
  11. console.log('edit setting')
  12. }
  13. }
  14. // user lands on home and we execute
  15. document.removeEventListener('keyup', homeShortcuts);
  16. document.addEventListener('keyup', settingsShortcuts);
  17.  
  18.  

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

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. observer.current = new IntersectionObserver(
  10. (entries) => {
  11. if (!entries[0].isIntersecting) {
  12. visible(true);
  13. } else {
  14. visbile(false);
  15. }
  16. },
  17. { rootMargin: `-${header.height}px` },
  18. );
  19. observer.current.observe(ref);
  20. }, [ref]);
  21.  
  22.  

上面的代码看起来不错。然而,一旦组件被卸载,观察者会发生什么?它不会被清除,那内存可就泄漏了。

我们怎么解决这个问题呢?只需要使用 disconnect 方法:

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

4. Window Object

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

看看下面的例子:

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

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

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

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

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

  1. "use strict"
  2.  
  3.  

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

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

  • 对于 addElement 函数,当从全局作用域调用时,this 是未定义的
  • 如果没有在一个变量上指定const | let | var,你会得到以下错误:
  1. Uncaught ReferenceError: b is not defined
  2.  
  3.  

5. 持有DOM引用

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

用一小段代码演示一下:

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

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

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

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


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

总结:

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

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

到此这篇关于常见的JavaScript内存错误的文章就介绍到这了,更多相关JavaScript内存错误内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号