用户从本地磁盘选择一张图片,网页立刻显示缩略图预览,这是各类后台管理系统和内容发布工具中高频出现的交互需求。看似简单的功能背后,其实涉及两条技术路线:一是FileReader的readAsDataURL方法,把文件内容编码成base64字符串直接塞给img标签;二是URL.createObjectURL生成一段临时的Blob链接,让浏览器直接从内存中读取文件数据。两条路线都能实现预览效果,但性能特征和适用场景差异明显。下面从文件输入控件的事件机制开始,逐层拆解完整实现方案。

从change事件到FileList:文件数据的进入方式
HTML中的<input type="file">元素是浏览器暴露本地文件系统的唯一入口。用户点击控件完成文件选择后,浏览器会触发change事件,事件对象的target.files属性是一个FileList对象。FileList长得像数组,但不是真正的数组,它没有forEach、map这些数组方法,只有length属性和item(index)方法,不过ES6的for...of循环可以直接遍历它。
FileList中的每个元素是File对象,File对象继承自Blob,因此拥有type(MIME类型)、size(字节数)、name(文件名)这些基础属性。如果没有给input添加multiple属性,用户每选择一次文件,change事件就触发一次,files列表的length始终为1。如果添加了multiple,用户可以一次选择多个文件,此时需要遍历files列表逐项处理。
<input type="file" id="fileInput" accept="image/*" />
const input = document.getElementById('fileInput');
input.addEventListener('change', function (event) {
const files = event.target.files;
if (!files.length) {
return;
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
console.log(file.name, file.type, file.size);
}
});
一个值得注意的细节是:用户连续选择同一个文件时,浏览器不会再次触发change事件,因为文件本身没有变化。这意味着如果你需要强制用户重复选择某个文件后重新触发处理逻辑,必须在选择完成后手动把input的value属性重置为空字符串。这个坑在稍后的图片预览场景中会直接导致重复选择相同图片时预览区域不刷新。
FileReader的readAsDataURL:最稳妥的浏览器兼容方案
FileReader是Web平台提供的异步文件读取接口,允许浏览器在不阻塞主线程的前提下读取文件内容。readAsDataURL方法将文件内容转化为base64编码的Data URL,这种URL以data:image/png;base64,开头,后面追加一段超长字符串。图片的mime类型信息已经包含在Data URL头部,因此img标签的src属性可以直接接收这个完整字符串进行渲染。
FileReader的所有读取方法都是异步的,需要通过事件监听器获取读取结果:onload表示读取成功,onerror表示读取失败。值得注意的是,如果处理得不够严谨,用户快速连续选择多个文件时,多个读取操作的回调可能会乱序返回,导致最后展示的图片与用户最后选择的文件不一致。解决办法是绑定当前文件与它的FileReader实例,或者使用闭包保存当前文件的引用。
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) {
return;
}
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
return;
}
const reader = new FileReader();
reader.onload = function (e) {
const img = document.createElement('img');
img.src = e.target.result;
img.alt = file.name;
document.body.appendChild(img);
};
reader.onerror = function () {
console.error('文件读取失败');
};
reader.readAsDataURL(file);
}
readAsDataURL的一个显著缺点是base64编码会让原始图片体积膨胀约33%。对于数百KB的小图片影响不大,但如果用户选择一张数MB的jpg文件,生成的Data URL会是一个数MB的超长字符串,导致DOM内存占用上升,页面可能出现明显卡顿。因此,对于追求性能的大图预览场景,更推荐使用下一节介绍的ObjectURL方案。
URL.createObjectURL:轻量级的内存引用方案
URL.createObjectURL(file)方法不会读取文件内容,也不会产生base64编码,它只是创建一个指向该File对象内存位置的短字符串引用。这个URL的格式通常是blob:http://localhost/一串随机UUID,浏览器内部维护着这个URL与文件对象的映射关系。img标签加载这个blob URL时,浏览器直接从内存中读取文件二进制数据,速度远快于base64的解析。
使用ObjectURL方案最大的好处是内存效率高,因为省去了base64编码带来的字符串复制过程。对于超大图片,这个差异尤其明显。但ObjectURL有一个必须注意的回收机制:blob URL会一直占用文件数据的内存,直到调用URL.revokeObjectURL方法主动释放,或者浏览器卸载页面。如果不及时释放,图片预览数量多时会积累大量内存占用,这在单页应用中可能造成隐患。
let currentObjectUrl = null;
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) {
return;
}
if (currentObjectUrl) {
URL.revokeObjectURL(currentObjectUrl);
}
currentObjectUrl = URL.createObjectURL(file);
const img = document.createElement('img');
img.src = currentObjectUrl;
img.onload = function () {
URL.revokeObjectURL(currentObjectUrl);
};
document.body.appendChild(img);
}
上面的代码在图片加载完成后立即调用revokeObjectURL释放内存。这里有一个常见误区:开发者担心revoke之后img就显示不了图片。实际上,一旦图片加载完成,浏览器已经把像素数据绘制到img元素上,撤销URL不会影响已经渲染的内容。但如果撤销过早(比如onload事件尚未触发就撤销),部分浏览器会出现图片加载失败的情况,因此要确保在onload回调里执行revoke。
格式校验与多图预览:更贴近实际项目的处理策略
在实际项目中,单图标配场景很少,更多见的是多图上传、预览以及删除。处理多图选择时,仅仅依靠accept属性还不够,因为accept只是浏览器文件选择对话框的推荐过滤,用户依然可以通过切换文件类型下拉框选择任意文件。真正的校验必须在change事件里读取file.type属性,用正则或startsWith方法判断是否属于图片类型。
多图预览的DOM插入策略也值得打磨:不要使用document.body.appendChild在页面末尾无限追加,而是把预览图统一插入到一个固定的容器中,方便后续管理。下面是一个包含格式过滤、多图遍历、DOM容器插入的完整示例。
<input type="file" id="multiInput" accept="image/*" multiple /> <div id="previewContainer" style="display: flex; flex-wrap: wrap; gap: 10px;"></div>
const multiInput = document.getElementById('multiInput');
const previewContainer = document.getElementById('previewContainer');
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
multiInput.addEventListener('change', function (event) {
const files = Array.from(event.target.files);
const validFiles = files.filter(
(file) => allowedTypes.includes(file.type) || file.type.startsWith('image/')
);
validFiles.forEach(function (file) {
const url = URL.createObjectURL(file);
const img = document.createElement('img');
img.src = url;
img.style.width = '120px';
img.style.height = '120px';
img.style.objectFit = 'cover';
img.style.borderRadius = '6px';
img.style.border = '1px solid #d0d7de';
img.addEventListener('load', function () {
URL.revokeObjectURL(url);
});
previewContainer.appendChild(img);
});
multiInput.value = '';
});
</script>
示例中的Array.from(event.target.files)把FileList转换为真正的数组,从而可以使用filter方法进行格式筛选。multiInput.value = ''这一行至关重要,它清空input的value,确保用户下次选择同一个文件时,change事件能够再次触发。这种处理方式避免了文件选择后预览区域不刷新的问题。
内存泄漏与性能优化:被忽略的关键细节
前面提到过ObjectURL的revoke时机,但实际开发中还有另一个隐含的内存问题:当预览图片从DOM中移除时,如果对应的blob URL尚未被revoke,文件数据仍然驻留在内存中。在动态内容很多的单页应用中,这个问题容易被忽略。比如一个图片列表支持用户单选删除,删除DOM节点时也应该同步revoke关联的URL。
另外一个性能优化点是大图片的压缩处理。对于头像类图片,如果原图是几MB的大文件,直接塞进image标签展示在100像素的圆形区域内,不仅浪费内存,而且加载速度慢。这种情况下,可以在展示前用canvas进行尺寸压缩,把图片缩放到目标尺寸后再显示。
function compressImage(file, maxWidth, maxHeight) {
return new Promise(function (resolve) {
const url = URL.createObjectURL(file);
const img = new Image();
img.src = url;
img.onload = function () {
let width = img.width;
let height = img.height;
if (width > maxWidth) {
height = Math.round(height * maxWidth / width);
width = maxWidth;
}
if (height > maxHeight) {
width = Math.round(width * maxHeight / height);
height = maxHeight;
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);
canvas.toBlob(
function (blob) {
resolve(URL.createObjectURL(blob));
},
file.type,
0.85
);
};
});
}
使用canvas压缩时,需要注意canvas本身会占用大量内存(宽高乘积乘以4字节),超大图片的压缩操作可能要消耗百MB级别的内存。因此压缩后的blob URL要妥善管理,在图片展示完成后及时revoke。这样一套流程,才能在真正的生产环境中平稳运行,不至于因为文件预览功能拖垮页面性能。
JavaScript文件输入DOM图片展示修改时间:2026-08-19 09:14:51