我有一个 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>
在小文件量下工作正常,但让我们回顾一下以下场景。
有什么聪明的方法可以防止剩余图像重新加载吗?
到目前为止,只是在沙箱中进行测试并寻找解决方案。
主要问题可能是
src={URL.createObjectURL(file.fileObject)}
。要理解为什么,我们首先看一下 URL.createObjectURL()
的文档:
静态方法创建一个字符串,其中包含表示参数中给定对象的 URL。URL.createObjectURL()
相关联。新对象 URL 表示指定的document
对象或File
对象。Blob
。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 条目。