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

React.js中使用requestAnimationFrame时,按键触发canvas动画无视觉反馈问题

React Canvas动画问题排查与解决方法

问题描述

尝试用React.js在canvas实现动画:按下空格键时,通过降低velocity值让红色方块先上升,再触发预设逻辑下落。目前通过console.log可见velocity值已改变、if语句已执行,但动画无对应效果。

原代码

JavaScript

const { useRef, useEffect, useState } = React;

const Canvas = () => {
  const { innerWidth: innerwidth, innerHeight: innerheight } = window;
  const contextRef = useRef()

  const [position, setPosition] = useState({ x: 100, y: 100 })
  const [size, setSize] = useState({ width: 30, height: 30 })
  const [velocity, setVelocity] = useState({ x: 0, y: 0 })

  const gravity = 4.5

  const draw = (context) => {
    context.fillStyle = 'red'
    context.fillRect(position.x, position.y, size.width, size.height);
  }; 

  const update = (context, canvas) => {
    draw(context)
    setPosition({ x:position.x, y:position.y += velocity.y })

    if (position.y + size.height +velocity.y <= canvas.height) {
      setVelocity({ x:velocity.x, y: velocity.y += gravity })
    } else {
      setVelocity({ x:velocity.x, y: velocity.y = 0 })
    }
  }

  const animate = (context, width, height, canvas) => {
    requestAnimationFrame(() => {
      animate(context, width, height, canvas )
    })
    context.clearRect(0, 0, width, height) 
    update(context, canvas)
  } 

  useEffect(() => {
    const canvas = contextRef.current;
    const context = canvas.getContext("2d");
    canvas.width = innerwidth - 2
    canvas.height = innerheight - 2
    animate(context, canvas.width, canvas.height, canvas)
  }, []);


const handleKeydown = (e) => {
  switch(e.keyCode) {
    case 37:
      console.log(velocity)
      return "left";
    case 39:
      return "right";
    case 32:   
      setVelocity({ x:velocity.x, y:velocity.y -= 50 });
      console.log(velocity)
      break
    default:
      console.log("keyCode is " + e.keyCode)
      return 'default';
    }    
  }

  return (
    <canvas ref={contextRef} tabIndex={-1} onKeyDown={(e) => {
      handleKeydown(e)
    }} />
  );
};

const App = () => <Canvas />;

ReactDOM
  .createRoot(document.getElementById("root"))
  .render(<App />);

CSS

html, body { width: 100%; height: 100%; margin: 0; }

HTML

<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

排查与解决方法

1. 修复闭包导致的状态获取失效

这是动画不更新的核心原因:useEffect依赖为空数组,启动的animate函数会捕获初始的position、velocity状态。后续状态更新后,animate仍在旧闭包中运行,始终使用初始值计算,完全无视状态变化。

解决方式:将状态从useState改为useRef存储,ref的current属性可随时获取最新值,无需依赖组件重新渲染:

// 替换原useState定义
const positionRef = useRef({ x: 100, y: 100 });
const sizeRef = useRef({ width: 30, height: 30 });
const velocityRef = useRef({ x: 0, y: 0 });

2. 修正状态更新的错误写法

原代码中position.y += velocity.y、velocity.y += gravity属于直接修改原对象,既不符合React状态更新规范,也会导致状态混乱。改用ref后直接更新current属性:

const update = (context, canvas) => {
  // 清除画布
  context.clearRect(0, 0, canvas.width, canvas.height);
  // 绘制方块
  context.fillStyle = 'red';
  context.fillRect(positionRef.current.x, positionRef.current.y, sizeRef.current.width, sizeRef.current.height);
  
  // 更新位置
  positionRef.current.y += velocityRef.current.y;

  // 更新速度(重力逻辑)
  if (positionRef.current.y + sizeRef.current.height + velocityRef.current.y <= canvas.height) {
    velocityRef.current.y += gravity;
  } else {
    velocityRef.current.y = 0;
  }
}

3. 调整键盘事件的状态更新逻辑

原键盘事件中直接修改velocity原对象,且因闭包无法获取最新状态值。改用ref后直接更新current:

const handleKeydown = (e) => {
  switch(e.keyCode) {
    case 32:   
      velocityRef.current.y -= 50;
      break
    // 其他case保持不变
    default:
      console.log("keyCode is " + e.keyCode)
      break;
    }    
}

4. 确保canvas能接收键盘事件

原代码中tabIndex={-1}允许canvas聚焦,但页面加载后canvas并未自动获取焦点,导致空格键可能无法触发事件。在useEffect中添加自动聚焦逻辑:

useEffect(() => {
  const canvas = contextRef.current;
  const context = canvas.getContext("2d");
  canvas.width = innerwidth - 2;
  canvas.height = innerheight - 2;
  canvas.focus(); // 自动获取焦点,确保键盘事件生效
  animate(context, canvas);
}, []);

修改后的完整代码

const { useRef, useEffect } = React;

const Canvas = () => {
  const { innerWidth: innerwidth, innerHeight: innerheight } = window;
  const contextRef = useRef()

  const positionRef = useRef({ x: 100, y: 100 });
  const sizeRef = useRef({ width: 30, height: 30 });
  const velocityRef = useRef({ x: 0, y: 0 });

  const gravity = 4.5

  const update = (context, canvas) => {
    // 清除画布
    context.clearRect(0, 0, canvas.width, canvas.height);
    // 绘制方块
    context.fillStyle = 'red';
    context.fillRect(positionRef.current.x, positionRef.current.y, sizeRef.current.width, sizeRef.current.height);
    
    // 更新位置
    positionRef.current.y += velocityRef.current.y;

    // 重力与落地逻辑
    if (positionRef.current.y + sizeRef.current.height + velocityRef.current.y <= canvas.height) {
      velocityRef.current.y += gravity;
    } else {
      velocityRef.current.y = 0;
    }
  }

  const animate = (context, canvas) => {
    requestAnimationFrame(() => animate(context, canvas));
    update(context, canvas);
  } 

  useEffect(() => {
    const canvas = contextRef.current;
    const context = canvas.getContext("2d");
    canvas.width = innerwidth - 2;
    canvas.height = innerheight - 2;
    canvas.focus();
    animate(context, canvas);
  }, []);

  const handleKeydown = (e) => {
    switch(e.keyCode) {
      case 32:   
        velocityRef.current.y -= 50;
        break
      default:
        console.log("keyCode is " + e.keyCode)
        break;
      }    
  }

  return (
    <canvas ref={contextRef} tabIndex={-1} onKeyDown={handleKeydown} />
  );
};

const App = () => <Canvas />;

ReactDOM
  .createRoot(document.getElementById("root"))
  .render(<App />);

内容的提问来源于stack exchange,提问作者tomtom

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 14:30:59