2025年下半年踩坑日志
总览
这是 2025年下半年踩坑日志 的有关内容。
1. URL.createObjectURL() 内存泄漏案例
内存泄漏原理
URL.createObjectURL(blob) 会创建一个指向内存中 Blob 对象的临时 URL。如果不手动释放该 URL,浏览器会一直保留对 Blob 的引用,即使 Blob 所在的组件 / 页面已被销毁,导致内存无法被垃圾回收。
使用案例
在一个需要展示很多图片的页面当中,想实现的效果:
- 提前下载图片到本地
- 用户滑动时瞬间显示,不用等网络
在这需求上使用URL.createObjectURL(blob)的优点是:
是性能优先,不需要base64转换,不需要网络请求。
灵活性:可以用在任何需要URL的地方(img、a、video等)
内存可控:提供了 revokeObjectURL() 让开发者手动管理(很多人不知道要配对使用 revokeObjectURL())
URL.createObjectURL() 会在内存中创建一个blob URL,但这个URL
不会被垃圾回收器自动清理。每次用户看新图片,就会创建新的blob URL,旧的永远不会被释放。也就是说每次用户滑动看新图片,就会重复下载并在内存中保存,旧的图片数据永远不会被清理。
为什么不自动回收?
因为实际上图片已经存储在内存当中了(blob),该URL只是一个指向该图片二进制内容的地址,相当于CPP里面的指针,所以浏览器也不知道你啥时候“用完了”这个URL。
const url = URL.createObjectURL(blob);
img1.src = url; // 第一个img在用
img2.src = url; // 第二个img也在用
someArray.push(url); // 可能还存在别的地方
// 所以需要手动清除
URL.revokeObjectURL(url);可以封装成组件直接使用(react案例)
// 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() 后所有关联的监听器会自动移除。
基础用法:单个事件监听器的销毁
// 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() 即可全部销毁:
// 创建控制器
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()
关键原理说明
AbortController 与 AbortSignal:
AbortController是控制器,通过signal属性暴露一个AbortSignal对象。AbortSignal是信号载体,事件监听器通过它与控制器关联。
销毁机制:
当调用controller.abort()时,signal会触发abort事件,所有通过{ signal }选项添加的事件监听器会被自动移除,且无法再被触发。注意事项:
- 一旦
abort()被调用,signal会进入“已中止”状态,无法重复使用(如需重新添加监听器,需创建新的AbortController)。 - 兼容性:所有现代浏览器均支持(IE 不支持,需兼容时可忽略)。
- 一旦
优势总结
- 简洁性:无需手动记录每个事件监听器的引用,避免
removeEventListener时的参数匹配问题。 - 统一性:一次调用即可销毁多个关联的监听器,尤其适合组件卸载、页面切换等场景。
- 安全性:防止因遗漏销毁导致的内存泄漏(如闭包中引用的事件处理函数无法被垃圾回收)。