<input type="file" id="el">
<code id="out"></code>
const el = document.getElementById('el');
const out = document.getElementById('out');
el.addEventListener('change', async () => {
const file = el.files?.[0];
if (file) {
const reader = file.stream().getReader();
out.innerText = JSON.stringify({ fileSize: file.size });
let received = 0;
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
// chunk.value.forEach((it) => it + 1);
received += chunk.value.byteLength;
out.innerText = JSON.stringify({
fileSize: file.size,
process: `${received} / ${file.size} (${((received / file.size) * 100).toFixed(2)}%)`,
});
}
}
});
上面的代码运行良好,
<code>
会实时显示进度。
但是如果我添加行chunk.value.forEach((it) => it + 1);
,主线程似乎被阻塞,页面停止响应,直到文件处理完成。 (在 Edge 125 中测试)
我可以使用
requestAnimationFrame
来修复它。
但为什么会这样呢,还有比requestAnimationFrame
更好的办法吗?
----编辑
chunk.value.forEach((it) => it + 1);
是真实代码的简化。我想做的是计算文件的md5。
浏览器似乎限制了每个块的大小,以将每个循环保持在 16 毫秒左右。额外的代码几乎不会影响时间和块大小
while(true) {
const start = Date.now();
// ...
console.log('chunk', Date.now() - start, chunk.value.byteLength);
}
// chunk 7 524288
// chunk 17 1572864
// chunk 25 2097152
// chunk 16 2097152
// chunk 18 2097152
// chunk 15 2097152
// chunk 16 2097152
// ...
恐怕这种行为实际上是符合规格的,即使它会带来糟糕的体验......
ReadableStream
控制器的拉动步骤
可以同步返回已经排队的块。因此,如果您在
read()
反应中的回调执行时间比文件系统对新块进行排队的时间长,则浏览器将永远不会将控制权处理回事件循环,并且您最终会陷入微任务循环,这在很大程度上是 UI阻塞。有趣的是,Firefox 似乎确实在某个地方排队了任务,因为与 Chrome 不同,它们不会阻止 UI。也许人们可以在规格层面上证明它是标准。 (Chrome 暴露阻塞行为已经有一段时间了,至少是 M84,所以它可能不会被视为一个大问题......)。
暂时,为了避免这种情况,您可以从回调中排队任务。为此,最快的方法是使用仍处于实验阶段的 方法,并优先考虑“用户阻止”,
await scheduler.postTask(() => {}, { priority: "user-blocking" });
对于仍然不支持此方法的浏览器,您可以使用 MessageChannel()
{
const { port1, port2 } = new MessageChannel();
globalThis.scheduler ??= {
postTask(cb, options) {
return new Promise((resolve, reject) => {
port1.addEventListener("message", () => {
try { resolve(cb()); } catch(err) { reject(err); }
}, { once: true });
port2.postMessage("");
port1.start();
});
}
};
}
// monkey-patch scheduler.postTask(cb, { priority: "user-blocking" })
{
const { port1, port2 } = new MessageChannel();
globalThis.scheduler ??= {
postTask(cb, options) {
return new Promise((resolve, reject) => {
port1.addEventListener("message", () => {
try { resolve(cb()); } catch(err) { reject(err); }
}, { once: true });
port2.postMessage("");
port1.start();
});
}
};
}
scheduler.postTask(() => {}).then(() => console.log("yep"))
const el = document.getElementById('el');
const out = document.getElementById('out');
el.addEventListener('change', async () => {
const file = el.files?.[0];
if (file) {
const reader = file.stream().getReader();
out.innerText = JSON.stringify({ fileSize: file.size });
let received = 0;
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
// Lock for 100ms
t1 = performance.now();
while(performance.now() - t1 < 100) {}
await scheduler.postTask(() => {}, { priority: "user-blocking" });
received += chunk.value.byteLength;
out.innerText = JSON.stringify({
fileSize: file.size,
process: `${received} / ${file.size} (${((received / file.size) * 100).toFixed(2)}%)`,
});
}
}
});
<input type="file" id="el">
<code id="out"></code>
但在你的情况下最好的实际上可能是使用
File
对象(或
Blob
)发送到工作上下文不会复制数据,因此您无需担心内存使用情况。