选择一张图片,用 URL.createObjectURL(file) 生成地址,再把它交给 img.src,预览就出现了。容易漏掉的是后半段:换图、清空、关闭预览时,谁负责释放之前生成的地址?
以米米商聊为背景,已整理的手机端资料包含照片发送。本文借图片选择场景讨论独立的浏览器预览示例,不代表产品采用 blob URL,也不推断原生 App 或桌面客户端的内部实现。示例只处理本地预览资源,不进行文件上传。
1. blob URL 是本地资源引用,不是上传结果
createObjectURL() 文档说明,这个方法为传入的 Blob 或 File 等对象创建一个可被其他 API 使用的地址。把地址交给图片元素,是让浏览器读取相应资源,不是把文件发送到服务端。
因此,“已创建预览地址”“图片已经加载”“上传请求已成功”是三个不同的状态。不能看到 blob: 字符串,就在界面上提示上传完成,也不应把它作为长期图片地址保存到聊天记录里。
| 操作或状态 | 能说明什么 | 不能直接说明什么 |
|---|---|---|
| 创建 object URL | 为对象建立了资源引用 | 文件已上传 |
给 img.src 赋值 |
图片元素开始使用该地址 | 图片内容一定可解码 |
| 图片加载成功 | 当前图片资源完成加载 | 服务端已经保存文件 |
| 调用 revoke | 释放这个 object URL 的引用 | 已取消上传或删除原文件 |
2. 从界面移除图片,不等于完成资源清理
组件不再显示图片,与 object URL 不再持有资源,是两件事。尤其在长时间打开的单页应用中,只切换组件而不卸载整个文档,容易让“以后浏览器会清理”的假设一直没有兑现。
MDN 的 blob URL 内存管理说明指出,同一个对象多次调用创建方法也会得到不同地址;不再需要时应分别释放。它还提醒不要在图片刚加载完就立即释放,否则可能影响用户继续保存或打开图片。
本文采用一个明确约定:一个控制器独占一个图片元素,只管理自己生成的地址;当前地址在预览期间保留,换图、主动清空或销毁时释放。地址不借给其他预览、下载按钮或新窗口使用。若多个消费者共享地址,释放时机必须重新设计,不能照搬这个独占模型。

3. 为预览建立一个明确的资源所有者
下面的控制器接收一个专用图片元素。setFile() 先创建新地址,再替换图片来源,最后释放旧地址;创建失败时,旧预览不会被提前清空。通过注入 api,可以独立检查创建与释放的调用顺序。
function createImagePreview(img, api = URL) {
let current = null;
let disposed = false;
function clear() {
img.removeAttribute("src");
const old = current;
current = null;
if (old !== null) api.revokeObjectURL(old);
}
return {
setFile(file) {
if (disposed) throw new Error("Preview has been disposed");
const next = api.createObjectURL(file);
try {
img.src = next;
} catch (error) {
api.revokeObjectURL(next);
throw error;
}
const old = current;
current = next;
if (old !== null) api.revokeObjectURL(old);
},
clear,
dispose() {
if (disposed) return;
clear();
disposed = true;
}
};
}
这里的“替换”只指来源赋值成功,不是新图片已经解码。控制器没有等待 load,也没有在解码失败后恢复旧图。若业务要求“新图确认可用后才替换”,需要另外设计预加载及失败回退流程,并分别管理预加载资源的释放。
clear() 同时断开图片来源并释放当前地址;重复清空不会重复释放同一个已登记地址。dispose() 表示控制器不再接受新文件,重复销毁也可安全返回。revokeObjectURL()负责 object URL 的释放,不负责业务请求或文件删除。
这个示例要求外部代码不要再写同一图片元素的 src,也不要同时绑定两个控制器。资源所有权一旦含糊,就容易出现一处仍在展示,另一处已经释放的情况。
4. 输入框、清空按钮和组件销毁都要接上
只在文件选择回调里创建控制器,会丢失旧控制器的引用。正确的接入方式是每个预览实例创建一次,在组件结束使用时执行清理函数。
function bindImagePicker(input, img, clearButton) {
const preview = createImagePreview(img);
const onChange = () => {
const file = input.files?.[0];
if (file) preview.setFile(file);
};
const onClear = () => {
preview.clear();
input.value = "";
};
input.addEventListener("change", onChange);
clearButton.addEventListener("click", onClear);
return () => {
input.removeEventListener("change", onChange);
clearButton.removeEventListener("click", onClear);
preview.dispose();
};
}
接入时使用文件输入框、专用 img 和 type="button" 的清空按钮。取消选择、没有新文件时,这份示例保留当前预览;点击清空时,同时重置文件输入值,方便之后再次选择同一个文件。
组件卸载时调用返回的函数。仅仅把弹窗设为隐藏,并不会自动执行这个函数:如果隐藏代表本次预览结束,应主动调用清空或销毁;如果隐藏后仍需恢复原预览,就继续保留资源,直到真的结束使用。这里需要产品行为与生命周期代码保持一致。
示例没有替代文件类型、体积和内容校验,也没有提供错误提示界面。实际应用应在文件入口完成相应检查,并把异常呈现给用户;不能把“生成地址成功”当作文件可用的证明。
5. 检查创建和释放是否成对,不用截图猜内存
独立验证从本文原样提取代码。首先使用可记录调用的 URL 替身,检查换图顺序、创建失败、赋值失败、新旧实例隔离和清理函数。再用当前内置浏览器与合成 PNG,核对真实 object URL 的加载、替换与释放行为。
| 验证场景 | 预期 |
|---|---|
| 首次选择 A | 创建 A 的地址,暂不释放 |
| 从 A 换成 B | 先让图片使用 B,再释放 A |
| 创建 B 的地址失败 | 保留 A,不提前释放 |
| 新来源赋值失败 | 释放新地址,保留旧登记 |
| 主动清空 | 移除图片来源,释放当前地址 |
| 连续清空、重复销毁 | 不重复释放已登记地址 |
| 销毁后再选择 | 不再创建新地址 |
| 两个独立预览 | 清理其中一个不影响另一个 |
浏览器检查使用合成图片,不读取用户相册、不发送上传请求;测试页面还核对释放后的旧地址不能再被重新读取。验证记录可以证明资源管理路径按预期执行,不能证明浏览器进程内存立即下降。
即使 revoke 已调用,其他对象引用、图片解码缓存或浏览器自身的回收时机,仍可能影响内存表现。若要报告内存改善,应额外设计可重复的长时测试与测量方法,不能用“关闭后页面没图了”或某一帧任务管理器数字代替。
6. 关闭动作应有一个对应的资源结束点
写预览功能时,可以把问题落到具体对象上:哪个实例创建了地址,谁还在使用它,换图时释放哪一个,组件销毁时如何收尾。每一次新建资源,都应能找到对应的生命周期终点。
本地预览、图片加载与业务上传分别管理,页面行为就更容易解释。释放地址是资源管理动作;上传取消、发送失败后保留草稿、文件删除,各自仍需要独立的业务规则。
本文为独立教学示例,部分内容由 AI 辅助整理,代码与发布内容经独立核对。