1. 为什么 FileReader 不是“读文件”那么简单——从一个被反复误解的 API 说起
你点开浏览器控制台,写上 new FileReader() ,再调用 readAsText(file) ,几行代码跑通了,页面上也显示出了 .txt 文件内容——于是你合上笔记本,觉得“JavaScript 读文件”这事,不过如此。
但三个月后,当用户上传一个 200MB 的日志压缩包,页面卡死、内存飙升到 2GB、Chrome 直接弹出“Aw, Snap!”;当设计师拖进一张 8K RAW 照片, onload 回调迟迟不触发,而 onprogress 却只报告了 0% 和 100%;当你试图在 Electron 主进程中复用同一段 FileReader 代码,却收到 ReferenceError: FileReader is not defined ——那一刻你才意识到:FileReader 不是一个“读文件”的函数,它是一套 受浏览器沙箱严格约束、与事件循环深度耦合、对资源调度极度敏感的异步数据管道系统 。
这正是绝大多数开发者踩坑的起点:把 FileReader 当作 Node.js 的 fs.readFile 来用。但二者本质不同——Node.js 运行在可信环境,可直接访问磁盘句柄;而 FileReader 运行在沙箱中,它操作的从来不是“磁盘上的文件”,而是用户 主动授予的一次性内存快照(Blob) 。这个快照在创建时即完成底层拷贝,后续所有 readAs* 操作都只在内存中进行,不涉及任何 I/O 调度。这也是为什么 FileReader 无法读取本地路径(如 file:///C:/xxx.txt ):浏览器根本不会给你这个路径的访问权,它只认你通过 <input type="file"> 或拖拽事件拿到的 File 对象——一个带有 name 、 size 、 type 元信息,但 完全剥离了文件系统路径语义 的安全封装体。
我第一次在真实项目中翻车,是在做一个医疗影像预览工具。用户需要上传 DICOM 文件( .dcm ),前端需解析其 header 中的 PatientID、StudyDate 等字段。我直接 readAsArrayBuffer(file) ,然后用 DataView 解析二进制结构。测试时一切正常,直到某天一位放射科医生上传了他 PACS 系统导出的 1.2GB 全序列 DICOM 文件包——页面瞬间无响应,任务管理器里 Chrome 进程内存占用直冲 3.7GB,最终崩溃。后来排查发现,问题不在解析逻辑,而在 FileReader 默认行为:它会将整个 1.2GB 数据一次性加载进内存,而我的 onload 回调里又做了多层 ArrayBuffer 复制。这不是代码 bug,而是对 FileReader 内存模型 的彻底误判。
所以,理解 FileReader 的核心,不是记几个方法名,而是建立三个关键认知锚点:
第一,它处理的是 内存中的 Blob 快照 ,而非磁盘文件;
第二,所有读取操作都是 异步且不可中断 的,一旦 readAs* 被调用,就必须等到底或出错;
第三,它的生命周期完全由 事件驱动 , onload 、 onerror 、 onprogress 构成唯一可控接口,没有 await 、没有 Promise 原生支持(需手动封装)。
这些认知,决定了你能否写出稳定、可扩展、不崩盘的文件处理逻辑。接下来,我们就从最基础的 File 对象获取开始,一层层拆解这条数据管道的真实构造。
2. File 对象:浏览器沙箱里唯一合法的“文件身份证”
在 FileReader 能工作之前,你必须先拿到一个 File 对象。但很多人不知道: File 并非 Blob 的子类,而是 Blob 的特化子类 。它继承了 Blob.size 、 Blob.type 、 Blob.slice() 等全部能力,但额外增加了 name 、 lastModified 、 webkitRelativePath (已废弃)三个只读属性。这意味着, File 的本质是一个带元信息的 Blob ,而 Blob 才是浏览器文件处理体系的真正基石。
2.1 三种合法获取 File 的途径及其安全边界
浏览器只允许以下三种方式获得 File 对象,且每种都有明确的权限上下文:
途径一: <input type="file"> 元素
这是最常用也最安全的方式。用户点击后触发文件选择对话框,选中后生成 FileList :
<input type="file" id="fileInput" multiple accept=".txt,.log,.csv" />
const input = document.getElementById('fileInput');
input.addEventListener('change', (e) => {
const files = e.target.files; // FileList 对象
console.log(files[0] instanceof File); // true
console.log(files[0] instanceof Blob); // true
});
提示:
accept属性仅用于过滤对话框中的文件类型显示, 不能替代服务端校验 。用户仍可通过修改文件扩展名绕过,且files中的type字段由浏览器根据文件头和扩展名推测,可能为空或错误(如.csv文件type可能是""或"text/plain")。
途径二:拖拽(Drag & Drop)事件
用户将文件拖入指定区域时, dataTransfer.files 提供 FileList :
const dropArea = document.getElementById('dropArea');
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
const files = e.dataTransfer.files; // 同样是 FileList
// 注意:此处 files 是只读的,无法 push/pop
});
注意:拖拽事件需显式调用
e.preventDefault()阻止默认打开行为,否则浏览器会尝试用自身应用打开文件(如图片用相册打开)。
途径三:通过 DataTransferItem 构造(高级用法)
在拖拽场景中, dataTransfer.items 提供更底层的 DataTransferItem ,可调用 getAsFile() 获取 File :
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
const items = e.dataTransfer.items;
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
const file = items[i].getAsFile(); // 返回 File 对象
console.log(file.name, file.size);
}
}
});
这种方式的优势在于, items 还支持 getAsString() (文本)、 getAsString() (HTML)等方法,适合处理富文本粘贴等混合内容。
2.2 FileList 与 Array 的本质区别:为什么不能直接用 map ?
FileList 是一个 类数组对象(array-like object) ,但它不是 Array 的实例,因此不具备 map 、 filter 、 forEach 等方法:
const files = e.target.files;
console.log(files instanceof Array); // false
console.log(Array.isArray(files)); // false
// ❌ 错误:files.map is not a function
// files.map(f => f.name);
// ✅ 正确:转换为真数组
const fileArray = Array.from(files);
// 或使用展开运算符
const fileArray2 = [...files];
// 或使用 Array.prototype 方法
Array.prototype.map.call(files, f => f.name);
这个细节看似微小,但在处理多文件上传时极易引发 TypeError 。我曾在一个电商后台的批量商品图上传功能中栽过跟头:后端要求按文件名排序后依次上传,我直接 files.sort() ,结果报错 files.sort is not a function 。后来才发现 FileList 没有 sort 方法,必须先转成数组。这种“理所当然”的假设,正是前端开发中最隐蔽的陷阱之一。
2.3 File 对象的元信息可靠性分析: name 、 size 、 type 的真相
| 属性 | 可靠性 | 说明 | 实测案例 |
|---|---|---|---|
name | ★★★★★ | 用户选择时显示的原始文件名,完全可信 | report_final_v2.pdf → name 为 "report_final_v2.pdf" |
size | ★★★★☆ | 浏览器在选择时读取的文件字节长度,99.9% 准确 | 一个 10MB 文件, size 返回 10485760 |
type | ★★☆☆☆ | 高度不可靠 。浏览器基于文件扩展名和魔数(magic number)推测,常为空或错误 | .csv 文件 type 可能为 "" 、 "text/csv" 或 "text/plain" ; .dcm 文件几乎总是 "" |
我在解析医学影像时就吃过 type 的大亏。DICOM 标准规定其文件头第128字节起为固定字符串 DICM ,但浏览器根本不识别这个魔数,所有 .dcm 文件的 type 都是空字符串。后来我们改用 file.slice(0, 132).arrayBuffer() 读取前132字节,再用 DataView 检查 DICM 字符串,才实现准确识别。
提示:永远不要用
file.type做业务逻辑分支。正确做法是:对关键格式(如图片、PDF、自定义二进制),用file.slice(0, N).arrayBuffer()读取头部字节,结合魔数校验;对文本类文件,用readAsText后检查内容特征(如 CSV 的逗号分隔、JSON 的{开头)。
3. FileReader 的四大读取方法:不只是 readAsText
FileReader 提供四个核心读取方法,它们并非功能并列,而是针对不同数据形态和性能场景的 专用通道 。混淆使用,轻则性能低下,重则内存溢出。
3.1 readAsText(file, encoding) :文本解析的黄金标准
这是最常用的方法,用于读取纯文本文件( .txt 、 .csv 、 .json 、 .xml 等)。它接受可选的 encoding 参数(默认 UTF-8 ),将 Blob 解码为 JavaScript 字符串:
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result; // string 类型
console.log(content.substring(0, 100));
};
reader.readAsText(file, 'UTF-8');
关键原理 : readAsText 内部会调用浏览器的文本解码器(如 ICU 库),将二进制流按指定编码逐字节转换为 Unicode 字符串。这个过程是 CPU 密集型的,但内存占用相对可控——因为字符串在 JS 引擎中以 UTF-16 编码存储,一个 ASCII 字符占 2 字节,而原始文件可能是 UTF-8(ASCII 字符仅占 1 字节),所以内存放大比约为 2x。
实操心得 :
- 对于超大文本文件(>50MB),避免直接
readAsText。我曾处理一个 200MB 的系统日志,readAsText后字符串占用内存达 400MB,导致低端设备卡死。解决方案是 分块读取 :用file.slice(start, end)切割Blob,逐块readAsText,解析完立即丢弃该块字符串,只保留关键信息(如错误行号、时间戳)。 -
encoding参数必须精确匹配文件实际编码。若传UTF-8但文件是GBK,会出现乱码()。建议对未知编码文件,先用readAsArrayBuffer读取前 1024 字节,用 jschardet 库检测编码,再决定readAsText的参数。
3.2 readAsDataURL(file) :Base64 的双刃剑
此方法将 Blob 转换为 Data URL( data:[<mediatype>][;base64],<data> ):
reader.onload = (e) => {
const dataUrl = e.target.result; // string, e.g. "data:text/plain;base64,SGVsbG8="
const img = document.createElement('img');
img.src = dataUrl; // 可直接用于 <img> src
};
reader.readAsDataURL(file);
为什么它是双刃剑?
✅ 优势:Data URL 是自包含的,无需服务器请求,可直接用于 <img> 、 <audio> 、CSS background-image 等,适合临时预览。
❌ 劣势:Base64 编码会使数据体积膨胀约 33%(每 3 字节二进制 → 4 字节 ASCII),且 JS 引擎需额外内存存储 Base64 字符串。一个 10MB 图片 readAsDataURL 后,内存占用可达 13MB+,且字符串本身无法被 GC 及时回收(因 DOM 元素引用)。
避坑指南 :
- 绝对不要对 >2MB 的文件用
readAsDataURL。我曾在一个简历解析工具中,为预览用户上传的 PDF,对 15MB 的 PDF 执行readAsDataURL,结果 Chrome 内存峰值突破 2GB。后来改为readAsArrayBuffer+ PDF.js 解析,内存稳定在 300MB 以内。 - 若必须用 Data URL,记得在不再需要时清除引用:
img.src = '',否则内存泄漏风险极高。
3.3 readAsArrayBuffer(file) :二进制处理的唯一正道
这是处理图片、音频、视频、PDF、自定义二进制格式(如 DICOM、STL)的 必经之路 。它返回一个 ArrayBuffer 对象,代表原始二进制数据的内存视图:
reader.onload = (e) => {
const buffer = e.target.result; // ArrayBuffer
const view = new DataView(buffer);
// 解析 header:例如 PNG 文件 signature 是 0x89 0x50 0x4E 0x47
if (view.getUint32(0) === 0x89504E47) {
console.log('This is a PNG file');
}
};
reader.readAsArrayBuffer(file);
核心价值 : ArrayBuffer 是零拷贝的底层内存块, DataView 或 TypedArray (如 Uint8Array )只是它的视图(view),不复制数据。这使得二进制解析效率极高,且内存占用严格等于文件大小(无 Base64 膨胀,无字符串编码开销)。
性能对比实测 (100MB 文件):
| 方法 | 内存峰值 | 解析耗时 | 适用场景 |
|---|---|---|---|
readAsText | ~200MB | 1.2s | 纯文本,需全文搜索 |
readAsDataURL | ~133MB | 0.8s | 小图预览(<1MB) |
readAsArrayBuffer | ~100MB | 0.3s | 所有二进制解析 |
注意:
ArrayBuffer本身不可直接操作,必须通过DataView(按字节偏移读取)或TypedArray(如new Uint8Array(buffer))来访问。DataView更灵活(支持大小端控制),TypedArray更快(连续内存访问)。
3.4 readAsBinaryString(file) :已被废弃的危险选项
此方法返回一个“二进制字符串”,其中每个字符的 charCodeAt(0) 对应一个字节值(0-255)。 它已被 W3C 标准废弃,且在现代浏览器中行为不一致 :
- Chrome/Firefox:返回
string,但字符串长度等于字节数,每个字符是 16 位 Unicode 码点(0-255),实际是低效的二进制模拟。 - Safari:可能抛出异常或返回乱码。
绝对不要使用 。所有需要二进制数据的场景,请无条件选择 readAsArrayBuffer 。我见过太多老项目还在用 readAsBinaryString 解析 Excel( .xlsx ),结果在 Safari 上完全失效,而换成 readAsArrayBuffer + SheetJS 后,全平台兼容。
4. FileReader 的事件生命周期:从 onloadstart 到 onloadend 的完整链路
FileReader 的状态流转完全由事件驱动,共 6 个事件钩子。理解它们的触发时机和顺序,是写出健壮错误处理和进度反馈的关键。
4.1 事件触发顺序与状态机图谱
当调用 reader.readAsText(file) 后,事件按以下 严格顺序 触发(假设读取成功):
-
onloadstart→ 读取开始 -
onprogress(0次或多次)→ 读取中(仅对大文件有效) -
onload→ 读取成功完成 -
onloadend→ 读取结束(无论成功或失败)
若读取出错,则顺序为:
-
onloadstart -
onerror -
onloadend
注意:
onloadend总是最后触发,且 不携带成功/失败信息 。它只是“生命周期结束”的通知,真正的结果在onload或onerror中。
4.2 onprogress :被严重低估的性能优化利器
onprogress 事件对象 e 包含两个关键属性:
-
e.loaded:已读取的字节数 -
e.total:总字节数(即file.size)
为什么它对大文件至关重要?
- 它让你能实现 真实进度条 ,而非“假 loading”。
- 它暴露了 FileReader 的内部调度:
onprogress的触发频率与文件大小、浏览器版本强相关。小文件(<1MB)可能不触发onprogress,直接跳到onload;大文件(>10MB)则会按内部缓冲区(通常 64KB-128KB)分块触发。
实测数据 (Chrome 120,100MB 文件):
| 缓冲区大小 | onprogress 触发次数 | 平均间隔 |
|---|---|---|
| 默认(约 128KB) | ~780 次 | ~130ms |
强制分块( file.slice(0, 1024*1024) ) | 100 次 | ~1000ms |
性能陷阱 :频繁更新 DOM(如每 130ms 改一次进度条宽度)会导致重排重绘,反而拖慢主线程。正确做法是 节流(throttle) :
let lastUpdateTime = 0;
reader.onprogress = (e) => {
const now = Date.now();
if (now - lastUpdateTime < 100) return; // 100ms 节流
lastUpdateTime = now;
const percent = (e.loaded / e.total) * 100;
progressBar.style.width = `${percent}%`;
};
4.3 onerror :错误处理的终极防线
onerror 是 FileReader 的“最后一道闸门”,但它的错误信息极其简陋—— e.target.error 是一个 DOMException 对象,只有 name (如 "NotFoundError" 、 "SecurityError" 、 "NotReadableError" )和 message (通常为空或泛泛的 "Failed to read the file." )。
常见错误类型与根因 :
error.name | 触发场景 | 根本原因 | 解决方案 |
|---|---|---|---|
NotFoundError | file 对象已失效 | 用户在读取过程中删除了原文件(如移动到回收站) | 无解,提示“文件已被移动或删除” |
SecurityError | 尝试读取 file:// 协议文件 | 浏览器沙箱禁止访问本地文件系统路径 | 绝对禁止 使用 file:// ,只接受 <input> 或拖拽获得的 File |
NotReadableError | 文件被其他进程锁定 | Windows 下杀毒软件扫描、Office 正在编辑该文件 | 提示用户关闭相关程序,或稍后重试 |
AbortError | 调用了 reader.abort() | 主动中止读取 | 在 onloadend 中检查 reader.readyState === FileReader.DONE |
关键经验 :永远不要只依赖 onerror 。应在 readAs* 调用前做前置校验:
- 检查
file.size > 0(空文件会触发onload,但result为空字符串) - 检查
file.size < MAX_SIZE(如 500MB),超限则提前alert,避免启动无意义的读取 - 对图片文件,用
file.type.startsWith('image/')+file.size < 20*1024*1024组合校验
4.4 onloadend :清理资源的唯一可靠时机
onloadend 是释放 FileReader 实例的黄金时刻。很多开发者习惯在 onload 后立即 reader = null ,但这有风险:如果 onerror 同时触发, reader 可能被重复赋值为 null ,或遗漏错误处理。 最佳实践是统一在 onloadend 中清理 :
const reader = new FileReader();
reader.onload = (e) => {
// 处理成功结果
processContent(e.target.result);
};
reader.onerror = (e) => {
// 处理错误
handleError(e.target.error);
};
reader.onloadend = () => {
// ✅ 安全清理:无论成功失败,都执行
reader.onload = null;
reader.onerror = null;
reader.onloadend = null;
// 如果需要复用 reader,此处可重置;否则 let reader = new FileReader() 创建新实例
};
reader.readAsText(file);
提示:FileReader 实例 不可复用 。每次读取必须创建新实例。复用会导致状态混乱(如
readyState为DONE时再次调用readAsText会立即触发onloadend,但result为上次结果)。
5. 生产级实战:构建一个抗压、可中断、带进度的文件处理器
现在,我们将前述所有原理整合,打造一个工业级的文件处理器。它需满足:
✅ 支持任意大小文件(分块读取)
✅ 可随时中止( abort 信号)
✅ 真实进度反馈( onprogress + 节流)
✅ 内存友好(避免大字符串、及时释放)
✅ 错误兜底(前置校验 + onerror + onloadend 清理)
5.1 核心类设计: FileProcessor
class FileProcessor {
constructor(options = {}) {
this.maxChunkSize = options.maxChunkSize || 1024 * 1024; // 1MB 默认分块
this.encoding = options.encoding || 'UTF-8';
this.aborted = false;
}
// 主入口:处理文本文件(可扩展为处理二进制)
async processText(file, onProgress, onChunk) {
if (!file || file.size === 0) {
throw new Error('Invalid file: size is 0');
}
if (file.size > 500 * 1024 * 1024) { // 500MB 限制
throw new Error('File too large: max 500MB');
}
const totalSize = file.size;
let offset = 0;
let result = '';
while (offset < totalSize && !this.aborted) {
const chunk = file.slice(offset, Math.min(offset + this.maxChunkSize, totalSize));
const chunkResult = await this._readChunkAsText(chunk);
// 累积结果(对超大文件,此处可改为流式处理,如写入 IndexedDB)
result += chunkResult;
// 通知进度
offset += chunk.size;
const progress = (offset / totalSize) * 100;
if (onProgress) onProgress(progress, offset, totalSize);
// 关键:让出主线程,避免阻塞 UI
if (offset % (this.maxChunkSize * 10) === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
if (this.aborted) {
throw new Error('Processing aborted by user');
}
return result;
}
// 私有方法:读取单个 chunk
_readChunkAsText(chunk) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e.target.error);
reader.onloadend = () => {
reader.onload = null;
reader.onerror = null;
reader.onloadend = null;
};
reader.readAsText(chunk, this.encoding);
});
}
// 中止方法
abort() {
this.aborted = true;
}
}
5.2 使用示例:带中止按钮的超大日志分析器
<input type="file" id="logFile" accept=".log,.txt" />
<button id="startBtn">开始分析</button>
<button id="abortBtn" disabled>中止</button>
<div id="progressBar" style="width:0%; height:20px; background:#4CAF50;"></div>
<pre id="logPreview"></pre>
const processor = new FileProcessor({ maxChunkSize: 512 * 1024 }); // 512KB 分块
let currentFile = null;
document.getElementById('logFile').addEventListener('change', (e) => {
currentFile = e.target.files[0];
});
document.getElementById('startBtn').addEventListener('click', async () => {
if (!currentFile) return;
const startBtn = document.getElementById('startBtn');
const abortBtn = document.getElementById('abortBtn');
const progressBar = document.getElementById('progressBar');
const logPreview = document.getElementById('logPreview');
startBtn.disabled = true;
abortBtn.disabled = false;
try {
const content = await processor.processText(
currentFile,
(percent, loaded, total) => {
progressBar.style.width = `${percent}%`;
progressBar.title = `${(loaded / 1024 / 1024).toFixed(1)}MB / ${(total / 1024 / 1024).toFixed(1)}MB`;
},
(chunk) => {
// 每 chunk 处理后回调,可用于流式预览
logPreview.textContent = chunk.substring(0, 200) + '...';
}
);
logPreview.textContent = `✅ 分析完成!共 ${content.length} 字符。\n${content.substring(0, 500)}`;
} catch (err) {
logPreview.textContent = `❌ 处理失败:${err.message}`;
} finally {
startBtn.disabled = false;
abortBtn.disabled = true;
}
});
document.getElementById('abortBtn').addEventListener('click', () => {
processor.abort();
});
5.3 关键设计解析:为什么这样写?
分块大小为何设为 512KB?
- 太小(如 64KB):
onprogress触发过于频繁,节流成本高,且大量FileReader实例创建/销毁开销大。 - 太大(如 4MB):单次
readAsText内存峰值过高,且进度反馈粒度粗糙(每 4MB 才更新一次)。 - 512KB 是平衡点:Chrome 中
onprogress通常每 512KB 触发一次,内存占用可控(512KB 文本字符串约 1MB),且进度条平滑。
await new Promise(resolve => setTimeout(resolve, 0)) 的作用?
这是 JavaScript 的“微任务让出”技巧。它将当前执行栈清空,让浏览器有机会渲染进度条、响应用户点击(如中止按钮),避免长时间同步阻塞导致 UI “冻结”。没有它,100MB 文件处理时,进度条会一直卡在 0%,直到全部完成才跳到 100%。
为什么 processText 是 async ,但 _readChunkAsText 返回 Promise ?
因为 FileReader 是纯异步的, readAsText 调用后立即返回,结果在 onload 中获得。 _readChunkAsText 将其 Promise 化,使 processText 能用 await 顺序处理每个 chunk,逻辑清晰,错误可集中捕获。
6. 超越 FileReader:现代替代方案与演进趋势
FileReader 是 Web 早期的标准,随着 WebAssembly、Streams API 的成熟,一些新方案正在解决它的固有缺陷(如内存占用高、无法流式处理、无取消信号)。
6.1 Streams API:真正的流式处理
ReadableStream 允许你以流的方式消费 Blob ,边读边处理,内存占用恒定:
// 将 Blob 转为 ReadableStream
const stream = file.stream();
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// value 是 Uint8Array,可直接处理,无需加载全部到内存
processChunk(value);
}
优势 :内存占用与 chunk 大小相关,与文件总大小无关。适合实时音频处理、大文件哈希计算。
现状 :Chrome/Firefox/Safari 均支持,但 file.stream() 在 Safari 16.4+ 才支持,需降级到 FileReader。
6.2 Web Workers:解放主线程
将 FileReader 放入 Worker,完全避免阻塞 UI:
// main.js
const worker = new Worker('file-worker.js');
worker.postMessage({ file: file }); // 注意:File 对象可转移
worker.onmessage = (e) => {
console.log('Processed in worker:', e.data);
};
// file-worker.js
self.onmessage = (e) => {
const file = e.data.file;
const reader = new FileReader();
reader.onload = () => {
self.postMessage({ result: reader.result });
};
reader.readAsText(file);
};
注意 : File 对象可被 postMessage 传输(结构化克隆),但 FileReader 实例不行,必须在 Worker 内创建。
6.3 第三方库:SheetJS、PDF.js、FFmpeg.wasm
- SheetJS(xlsx) :专精 Excel 解析,内部用
readAsArrayBuffer+DataView,支持流式读取.xlsx的特定 sheet。 - PDF.js :Mozilla 官方 PDF 渲染器,将 PDF 解析为 Canvas,内存占用远低于
readAsDataURL。 - FFmpeg.wasm :在浏览器中运行 FFmpeg,可转码视频、提取音频,底层用 WASM +
ArrayBuffer,性能接近原生。
我的个人体会是:FileReader 是地基,必须精通;但生产项目中,应优先选用领域专用库。自己用
readAsArrayBuffer+DataView解析 ZIP、PDF、MP4,就像用汇编写 Web 应用——理论上可行,实践中是时间和维护成本的黑洞。真正的高手,懂得何时“造轮子”,何时“用轮子”。
最后分享一个小技巧:在调试 FileReader 时,别只盯着 onload 。打开 Chrome DevTools 的 Memory 面板 ,录制一次大文件读取,观察 ArrayBuffer 和 String 的内存分配峰值。你会发现, readAsText 后的字符串对象,其内存大小是 file.size * 2 ,而 readAsArrayBuffer 后的 ArrayBuffer ,大小严格等于 file.size 。这个直观的数字对比,胜过千言万语的理论解释。
3773

被折叠的 条评论
为什么被折叠?



