我有一个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张图像,每张图像大小约为2 MB,因此我们已上传约20 MB的数据
  • 单击一张图像将其删除
  • 重新渲染9张图像,网络选项卡再次重新加载9张图像,因此总共需要19个请求来完成此操作.

Network tab showing the problem

有没有巧妙的方法可以防止剩余图像重新加载?

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

推荐答案

主要问题可能是src={URL.createObjectURL(file.fileObject)}.要理解为什么让我们先看看URL.createObjectURL()的文档:

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

URL生命周期与创建它的窗口中的document绑定.新对象URL表示指定的File对象或Blob对象.

要释放对象URL,请调用revokeObjectURL().

正如您所看到的那样,URL的生命周期为document.在您的场景中,每次渲染App时,都会创建新的URL,而不会释放旧的URL.即使每当卸载App时,创建的URL也会继续存在.

您可以通过判断图像来看到这一点.每当您删除单个图像时,所有剩余图像都会被分配一个新的src.

要解决这个问题,不要在渲染上创建URL,而是在将图像添加到状态时创建URL,就像处理id一样.然后,每当图像被删除时,就撤销该URL.(而且您很可能也想在卸载组件时撤销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 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={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挂钩.这将创建一个对于identity is stable(对象引用永远不会更改)的组件的此实例唯一的对象.这个ref保存一个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条目,这使得添加/删除文件变得更加复杂.

Javascript相关问答推荐

我想做一个程序,计算字符串中所有单词的数量

如何在非独立组件中使用独立组件?

为什么我的第二个OnClick Isloading值在TEK查询Mutations 查询中不起作用?

如何让\w token 在此RegEx中表现得不贪婪?

为什么在集内和集外会产生不同的promise 状态(使用Promise.race)?

如何访问react路由v6加载器函数中的查询参数/搜索参数

在JavaScript中对大型级联数组进行切片的最有效方法?

从实时数据库(Firebase)上的子类别读取数据

vscode扩展-webView Panel按钮不起任何作用

扫描qr code后出错whatter—web.js

为什么这个JS模块在TypeScript中使用默认属性导入?""

将旋转的矩形zoom 到覆盖它所需的最小尺寸&S容器

在JS中拖放:检测文件

在forEach循环中获取目标而不是父对象的属性

如何使用JavaScript拆分带空格的单词

在浏览器中触发插入事件时检索编码值的能力

使用插件构建包含chart.js提供程序的Angular 库?

不同表的条件API端点Reaction-redux

无法使用npm install安装react-dom、react和next

将Singleton实例设置为未定义后的Angular 变量引用持久性行为