添加/删除时反应图像上传重新渲染上传的图像

问题描述 投票:0回答:1

我有一个 React 代码,可以让您添加图像文件、预览、单击删除以及添加更多。我对这个功能很满意,但我注意到一些性能问题。

function App() {
  const [selectedFiles, setSelectedFiles] = React.useState([])

  function GenerateGuid() {
    return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => {
      const randomValue = crypto.getRandomValues(new Uint8Array(1))[0];
      const shiftedValue = (randomValue & 15) >> (+c / 4);
      return shiftedValue.toString(16);
    });
  }

  const handleImageDelete = (id) => {
    const updatedFiles = selectedFiles.filter((file) => file.id !== id);
    setSelectedFiles(updatedFiles);
  };

  const handleChange = (e) => {
    const files = Array.from(e.target.files)
    const filesObject = files.map(file => {
      return (
        { id: GenerateGuid(), fileObject: file }
      )
    })
    setSelectedFiles([...selectedFiles, ...filesObject])
  }

  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <h2>Start editing to see some magic happen!</h2>
      <input accept="image/png, image/gif, image/jpeg" type="file" onChange={handleChange} multiple />
      {selectedFiles.map(
        (file, index) => {
          return (
            <img
              key={file.id}
              onClick={() => handleImageDelete(file.id)}
              style={{ height: "5rem", backgroundColor: "black" }}
              src={URL.createObjectURL(file.fileObject)}
              alt={"training spot"}
            />
          )
        }
      )}
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

在小文件量下工作正常,但让我们回顾一下以下场景。

  • 添加 10 张图像,每张图像约 2MB 大小,因此我们上传了约 20MB 的数据
  • 点击一张图片即可将其删除
  • 9 个图像重新渲染,网络选项卡显示 9 个图像再次重新加载,因此总共有 19 个请求来完成此操作。

Network tab showing the problem

有什么聪明的方法可以防止剩余图像重新加载吗?

到目前为止,只是在沙箱中进行测试并寻找解决方案。

javascript reactjs react-tsx
1个回答
0
投票

主要问题可能是

src={URL.createObjectURL(file.fileObject)}
。要理解为什么,我们首先看一下
URL.createObjectURL()
的文档:

URL.createObjectURL()
静态方法创建一个字符串,其中包含表示参数中给定对象的 URL。

URL 生命周期与创建它的窗口中的

document
相关联。新对象 URL 表示指定的
File
对象或
Blob
对象。

要释放对象 URL,请调用

revokeObjectURL()

如您所见,URL 具有

document
的生命周期。在您的场景中,每次渲染
App
时,都会创建新的 URL,而不释放旧的 URL。即使每当
App
被卸载时,创建的 URL 仍然存在。

要解决此问题,请不要在渲染时创建 URL,而是在将图像添加到状态时创建 URL,就像使用 ids 一样。然后,每当卸载组件时,请确保同时撤销 URL。

function App() {
  const [selectedFiles, setSelectedFiles] = React.useState([]);
  const urls = React.useRef(new Set());

  const handleImageDelete = (id) => {
    setSelectedFiles(selectedFiles.filter((file) => {
      if (file.id !== id) return true; // keep in state
      // delete URL from register (Set) and revoke it
      urls.current.delete(file.url);
      URL.revokeObjectURL(file.url);
      return false; // remove from state
    }));
  };

  const handleChange = (e) => {
    const newFiles = Array.from(e.target.files, (fileObject) => {
      const id  = crypto.randomUUID();
      // create URL and add it to the register (Set)
      const url = URL.createObjectURL(fileObject);
      urls.current.add(url);
      return { id, url, fileObject };
    });
    setSelectedFiles([...selectedFiles, ...newFiles])
  }
  
  React.useEffect(() => {
    // revoke register URLs on unmount
    return () => {
      for (const url of urls.current) URL.revokeObjectURL(url);
    };
  }, []);

  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <h2>Start editing to see some magic happen!</h2>
      <input type="file" onChange={handleChange} multiple />
      {selectedFiles.map(
        (file, index) => {
          return (
            <img
              key={file.id}
              onClick={() => handleImageDelete(file.id)}
              style={{ height: "5rem", backgroundColor: "black" }}
              src={file.url}
              alt={"training spot"}
            />
          )
        }
      )}
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

我已将

GenerateGuid
替换为
crypto.randomUUID()
,以将代码片段简化为仅相关代码。

在上面的代码中,您将看到每当文件添加到文件列表时都会生成 URL。它不再为每个渲染生成。每当图像被删除时,我也会撤销该 URL。这些变化大有帮助。

但是,为了确保每当组件卸载时 URL 都会被撤销,我们还必须添加一个

useEffect
钩子。一开始你可能会想做这样的事情:

React.useEffect(() => {
  return () => {
    selectedFiles.forEach(file => URL.revokeObjectURL(file.url));
  };
}, [selectedFiles]);

但是上面的方法不起作用,因为它不仅在卸载时运行,而且在

selectedFiles
更改时运行。这意味着每当您添加文件时,都会创建 URL,并在渲染后立即撤销。

为了解决这个问题,我使用了

useRef
钩子。这将创建一个对于该组件实例来说是唯一的对象,该组件的句柄是稳定的(对象引用永远不会改变)。该引用包含一个
Set
实例,我将其用作 URL 寄存器。这允许我们访问 URL,而无需为
useEffect
钩子指定依赖项。

React.useEffect(() => {
  // revoke register URLs on unmount
  return () => {
    for (const url of urls.current) URL.revokeObjectURL(url);
  };
}, []); // <- no dependencies, the returned function only runs on unmount

这确实意味着,每当我们创建或撤销 URL 时,我们都需要在寄存器中添加或删除 URL 条目。

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