Skip to content

2025年下半年踩坑日志🔥

总览

这是 2025年下半年踩坑日志 的有关内容。

1. URL.createObjectURL() 内存泄漏案例

内存泄漏原理

URL.createObjectURL(blob) 会创建一个指向内存中 Blob 对象的临时 URL。如果不手动释放该 URL,浏览器会一直保留对 Blob 的引用,即使 Blob 所在的组件 / 页面已被销毁,导致内存无法被垃圾回收。

使用案例

在一个需要展示很多图片的页面当中,想实现的效果:

  • 提前下载图片到本地
  • 用户滑动时瞬间显示,不用等网络

在这需求上使用URL.createObjectURL(blob)的优点是:

  1. 是性能优先,不需要base64转换,不需要网络请求。

  2. 灵活性:可以用在任何需要URL的地方(img、a、video等)

  3. 内存可控:提供了 revokeObjectURL() 让开发者手动管理(很多人不知道要配对使用 revokeObjectURL())

URL.createObjectURL() 会在内存中创建一个blob URL,但这个URL不会被垃圾回收器自动清理。每次用户看新图片,就会创建新的blob URL,旧的永远不会被释放。也就是说每次用户滑动看新图片,就会重复下载并在内存中保存,旧的图片数据永远不会被清理。

为什么不自动回收?

因为实际上图片已经存储在内存当中了(blob),该URL只是一个指向该图片二进制内容的地址,相当于CPP里面的指针,所以浏览器也不知道你啥时候“用完了”这个URL。

JS
const url = URL.createObjectURL(blob);
img1.src = url; // 第一个img在用
img2.src = url; // 第二个img也在用
someArray.push(url); // 可能还存在别的地方

// 所以需要手动清除
URL.revokeObjectURL(url);

可以封装成组件直接使用(react案例)

js
// useObjectURL.js
import { useRef, useEffect } from 'react';

export function useObjectURL(blob) {
  const urlRef = useRef('');

  // 创建 URL
  useEffect(() => {
    if (!blob) return;
    
    // 释放旧 URL
    if (urlRef.current) {
      URL.revokeObjectURL(urlRef.current);
    }
    
    const url = URL.createObjectURL(blob);
    urlRef.current = url;
    
    return () => {
      // 组件卸载时释放 URL
      URL.revokeObjectURL(url);
    };
  }, [blob]);

  return urlRef.current;
}

// 使用示例
function ImagePreview() {
  const [selectedFile, setSelectedFile] = useState(null);
  const imageUrl = useObjectURL(selectedFile);

  const handleFileSelect = (e) => {
    setSelectedFile(e.target.files[0]);
  };

  return (
    <div>
      <input type="file" onChange={handleFileSelect} />
      {imageUrl && <img src={imageUrl} alt="Preview" />}
    </div>
  );
}

2. 事件监听器如何统一销毁

使用 AbortController 可以统一管理多个事件监听器的销毁,避免手动调用 removeEventListener 时的遗漏或繁琐操作。核心原理是通过 AbortSignal 关联事件监听器,调用 abort() 后所有关联的监听器会自动移除。

基础用法:单个事件监听器的销毁

javascript
// 1. 创建 AbortController 实例
const controller = new AbortController();
// 获取关联的 signal 对象
const { signal } = controller;

// 2. 添加事件监听器时,通过 options 传入 signal
document.getElementById('btn').addEventListener('click', handleClick, { signal });

// 3. 当需要销毁时,调用 abort()
function destroy() {
  controller.abort(); // 所有关联了该 signal 的监听器会被自动移除
}

// 事件处理函数
function handleClick() {
  console.log('按钮被点击');
}

进阶用法:统一管理多个事件监听器

多个事件监听器可以关联同一个 signal,调用一次 abort() 即可全部销毁:

javascript
// 创建控制器
const controller = new AbortController();
const { signal } = controller;

// 1. 给按钮添加点击事件
document.getElementById('btn').addEventListener('click', handleClick, { signal });

// 2. 给窗口添加滚动事件
window.addEventListener('scroll', handleScroll, { signal });

// 3. 给输入框添加输入事件
document.getElementById('input').addEventListener('input', handleInput, { signal });

// 统一销毁函数
function destroyAll() {
  controller.abort(); // 一次性移除上述所有事件监听器
  console.log('所有事件监听器已销毁');
}

// 事件处理函数(示例)
function handleClick() { /* ... */ }
function handleScroll() { /* ... */ }
function handleInput() { /* ... */ }

注意:controller.abort()销毁后,这个controller就被清除了,如果需要重新监听需要重新new AbortController()

关键原理说明

  1. AbortController 与 AbortSignal

    • AbortController 是控制器,通过 signal 属性暴露一个 AbortSignal 对象。
    • AbortSignal 是信号载体,事件监听器通过它与控制器关联。
  2. 销毁机制
    当调用 controller.abort() 时,signal 会触发 abort 事件,所有通过 { signal } 选项添加的事件监听器会被自动移除,且无法再被触发。

  3. 注意事项

    • 一旦 abort() 被调用,signal 会进入“已中止”状态,无法重复使用(如需重新添加监听器,需创建新的 AbortController)。
    • 兼容性:所有现代浏览器均支持(IE 不支持,需兼容时可忽略)。

优势总结

  • 简洁性:无需手动记录每个事件监听器的引用,避免 removeEventListener 时的参数匹配问题。
  • 统一性:一次调用即可销毁多个关联的监听器,尤其适合组件卸载、页面切换等场景。
  • 安全性:防止因遗漏销毁导致的内存泄漏(如闭包中引用的事件处理函数无法被垃圾回收)。