You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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"可正常显示。


问题原因与解决方案

问题根源

  1. 图片未加载完成就触发绘制:创建Image实例后直接存入状态,但图片是异步加载的,此时img可能处于未就绪状态。当render循环尝试绘制未加载完成的图片时,就会抛出"broken"状态错误。
  2. 闭包导致的状态引用失效: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 10:35:11