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
相关产品推荐
相关产品推荐

