React中为Canvas传入图片序列报错?原因排查与优化方案
问题描述
我有150张命名为0001.jpg、0002.jpg……0150.jpg的图片,用React实现逐帧渲染时遇到了Canvas绘制错误。以下是相关代码片段:
相关代码
import React, { useRef, useEffect, useState } from 'react'; function getCurrentFrame(index) { return `../${index.toString().padStart(4, "0")}.jpg`; } const ImageCanvas = ({ scrollHeight, numFrames, width, height }) => { const canvasRef = useRef(null); const [images, setImages] = useState([]); const [frameIndex, setFrameIndex] = useState(0); // Step 1: Load images function preloadImages() { for (let i = 1; i <= numFrames; i++) { const img = new Image(); const imgSrc = getCurrentFrame(i); img.src = imgSrc; setImages((prevImages) => [...prevImages, img]); } }
const render = () => { console.log(images[frameIndex]); context.drawImage(images[frameIndex], 0, 0); requestId = requestAnimationFrame(render); }; render();
错误信息
"Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.
at render"
console.log(images[frameIndex])会先打印3-5次正确的元素<img src="../0001.jpg">,之后才出现错误。已确认图片路径正确,直接设置img.src为"../0001.jpg"可正常显示。
问题原因与解决方案
问题根源
- 图片未加载完成就触发绘制:创建
Image实例后直接存入状态,但图片是异步加载的,此时img可能处于未就绪状态。当render循环尝试绘制未加载完成的图片时,就会抛出"broken"状态错误。 - 闭包导致的状态引用失效:
render循环捕获的是旧版本的images数组,后续setImages生成新数组后,旧数组里的img引用可能出现异常。
优化实现方式
1. 确保图片加载完成后再存入状态
用Promise封装图片加载逻辑,等待所有图片加载完成后再更新状态,避免绘制未就绪的资源:
import React, { useRef, useEffect, useState } from 'react'; function getCurrentFrame(index) { return `../${index.toString().padStart(4, "0")}.jpg`; } // 封装图片加载Promise function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } const ImageCanvas = ({ scrollHeight, numFrames, width, height }) => { const canvasRef = useRef(null); const [images, setImages] = useState([]); const [frameIndex, setFrameIndex] = useState(0); const requestIdRef = useRef(null); // 用ref存储requestId,避免闭包问题 useEffect(() => { // 批量预加载所有图片 const preloadAllImages = async () => { const imagePromises = []; for (let i = 1; i <= numFrames; i++) { imagePromises.push(loadImage(getCurrentFrame(i))); } try { const loadedImages = await Promise.all(imagePromises); setImages(loadedImages); } catch (err) { console.error('图片加载失败:', err); } }; preloadAllImages(); // 清理动画 return () => { requestIdRef.current && cancelAnimationFrame(requestIdRef.current); }; }, [numFrames]); useEffect(() => { // 图片未加载完成时不启动渲染 if (images.length === 0) return; const canvas = canvasRef.current; if (!canvas) return; const context = canvas.getContext('2d'); const render = () => { const currentImg = images[frameIndex]; // 确保图片已加载完成再绘制 if (currentImg && currentImg.complete) { context.drawImage(currentImg, 0, 0); } requestIdRef.current = requestAnimationFrame(render); }; requestIdRef.current = requestAnimationFrame(render); return () => { requestIdRef.current && cancelAnimationFrame(requestIdRef.current); }; }, [images, frameIndex]); return <canvas ref={canvasRef} width={width} height={height} />; };
2. 关键优化点
- 用
Promise.all批量加载图片,确保所有资源就绪后再进入渲染流程。 - 用
useRef存储requestId,避免闭包捕获旧值导致无法正确取消动画。 - 渲染循环中增加
currentImg.complete判断,额外保障绘制的图片处于就绪状态。 - 拆分
useEffect,将图片加载和渲染逻辑分离,避免不必要的重复执行。
内容的提问来源于stack exchange,提问作者pcmila
相关产品推荐
相关产品推荐

