React应用中如何在requestAnimationFrame循环内使用响应式变量?
解决React中Canvas动画无法响应Leva控件更新的问题
核心问题是闭包捕获导致的旧值引用:你的render函数是在初始的useEffect(依赖为空数组)中创建的,它捕获的是组件第一次渲染时的size值。之后Leva控件更新size时,这个旧的render函数并不会重新生成,所以动画循环里始终用的是初始的size,而非最新值。
解决方案1:用Ref同步最新size值(推荐,不中断动画)
通过useRef保存最新的size数值,让render函数能直接访问到更新后的值,同时保持动画循环不中断:
import { useRef, useEffect } from "react"; import { useControls } from 'leva'; export default function SquareCanvas() { const canvasRef = useRef(null); const { size } = useControls({ size: { value: 10, min: 1, max: 50}, }); // 创建ref同步最新size const sizeRef = useRef(size); let canvas, ctx; // 保存动画ID用于清理 const animationIdRef = useRef(null); // 监听size变化,实时更新ref的current值 useEffect(() => { sizeRef.current = size; }, [size]); useEffect(() => { canvas = canvasRef.current; ctx = canvas.getContext('2d'); setSize(); const handleResize = setSize; window.addEventListener('resize', handleResize); ctx.fillStyle = 'red'; function render(time) { ctx.clearRect(0, 0, canvas.width, canvas.height); const x = canvas.width / 2 + Math.sin(time * .005) * 100; const y = canvas.height / 2; // 使用ref中的最新size值 ctx.fillRect(x, y, sizeRef.current, sizeRef.current); animationIdRef.current = requestAnimationFrame(render); } animationIdRef.current = requestAnimationFrame(render); // 清理函数:移除监听并取消动画,避免内存泄漏 return () => { window.removeEventListener('resize', handleResize); cancelAnimationFrame(animationIdRef.current); } }, []); function setSize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } return <div><canvas ref={canvasRef}></canvas>{ size }</div> }
解决方案2:将size加入useEffect依赖(动画会重启)
如果可以接受动画在size变化时短暂重启,也可以把size加入useEffect的依赖数组,每次size更新时重新初始化动画循环:
import { useRef, useEffect } from "react"; import { useControls } from 'leva'; export default function SquareCanvas() { const canvasRef = useRef(null); const { size } = useControls({ size: { value: 10, min: 1, max: 50}, }); let canvas, ctx; const animationIdRef = useRef(null); useEffect(() => { canvas = canvasRef.current; ctx = canvas.getContext('2d'); setSize(); const handleResize = setSize; window.addEventListener('resize', handleResize); ctx.fillStyle = 'red'; function render(time) { ctx.clearRect(0, 0, canvas.width, canvas.height); const x = canvas.width / 2 + Math.sin(time * .005) * 100; const y = canvas.height / 2; ctx.fillRect(x, y, size, size); animationIdRef.current = requestAnimationFrame(render); } animationIdRef.current = requestAnimationFrame(render); return () => { window.removeEventListener('resize', handleResize); cancelAnimationFrame(animationIdRef.current); } }, [size]); // 加入size作为依赖,触发重新初始化 function setSize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } return <div><canvas ref={canvasRef}></canvas>{ size }</div> }
关键说明
- 方案1通过
sizeRef同步最新值,动画循环不会中断,体验更流畅,适合需要持续播放的动画场景。 - 方案2每次size更新都会重新创建
render函数并重启动画,适合对动画连续性要求不高的场景。 - 两种方案都添加了动画和resize监听的清理逻辑,避免内存泄漏。
内容的提问来源于stack exchange,提问作者TheCat
相关产品推荐
相关产品推荐

