为什么异步循环 `File.stream().getReader().read()` 可能会阻塞主线程?

问题描述 投票:0回答:1
<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
// ...
javascript html dom w3c
1个回答
0
投票

恐怕这种行为实际上是符合规格的,即使它会带来糟糕的体验......

ReadableStream控制器的拉动步骤

可以同步返回已经排队的块。因此,如果您在 
read()
 反应中的回调执行时间比文件系统对新块进行排队的时间长,则浏览器将永远不会将控制权处理回事件循环,并且您最终会陷入微任务循环,这在很大程度上是 UI阻塞。

有趣的是,Firefox 似乎确实在某个地方排队了任务,因为与 Chrome 不同,它们不会阻止 UI。也许人们可以在规格层面上证明它是标准。 (Chrome 暴露阻塞行为已经有一段时间了,至少是 M84,所以它可能不会被视为一个大问题......)。

暂时,为了避免这种情况,您可以从回调中排队任务。为此,最快的方法是使用仍处于实验阶段的

scheduler.postTask()

 方法,并优先考虑“用户阻止”,

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>

但在你的情况下最好的实际上可能是使用

Web Worker

并将文件发送到那里,因为即使通过在循环中排队任务我们让事件循环稍微喘息一下,它仍然很难在可用时间如此之少的情况下处理所有 UI 更新。请注意,将

File

 对象(或 
Blob
)发送到工作上下文不会复制数据,因此您无需担心内存使用情况。

© www.soinside.com 2019 - 2024. All rights reserved.