如何避免React在鼠标位置更新时重复重绘Canvas?
问题分析
你的问题核心在于多个并行的requestAnimationFrame渲染循环同时运行,导致气泡被多次驱动移动,看起来速度异常。具体原因:
draw函数在组件每次重渲染(比如mousePos更新时)都会被重新创建,而渲染逻辑的useEffect依赖draw,所以每次draw变化都会启动新的渲染循环,鼠标高频移动时会短时间内存在多个并行循环。draw函数内每次渲染都重置canvas宽高,既浪费性能,也会干扰canvas渲染状态。
优化方案
1. 稳定draw函数,避免频繁重建
用useCallback包裹draw函数,明确依赖项,确保仅在依赖变化时重新创建:
const draw = useCallback((ctx: CanvasRenderingContext2D) => { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); bubbles.forEach((bubble) => { bubble.intersectWall(); bubble.move(); bubble.draw(mousePos); }); }, [bubbles, mousePos]);
将mousePos作为draw的依赖而非参数传递,既保证鼠标位置变化时能正确绘制瞳孔,也让draw的定义更稳定。
2. 调整渲染循环的触发逻辑
将渲染useEffect的依赖改为稳定后的draw,同时把canvas尺寸调整剥离到resize事件中:
useEffect(() => { const canvas = canvasRef.current as HTMLCanvasElement; const context = canvas.getContext("2d") as CanvasRenderingContext2D; // 单独处理canvas尺寸适配 const resizeCanvas = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }; resizeCanvas(); window.addEventListener('resize', resizeCanvas); let animationFrameId: number; const render = () => { draw(context); animationFrameId = window.requestAnimationFrame(render); }; render(); return () => { window.cancelAnimationFrame(animationFrameId); window.removeEventListener('resize', resizeCanvas); }; }, [draw]);
这样避免了每次渲染都重置canvas尺寸,减少不必要的性能损耗。
3. 优化鼠标位置更新逻辑
将screenX/screenY替换为clientX/clientY(更符合canvas视口坐标系),同时用useCallback稳定事件处理函数:
const handleMouseMove = useCallback((e: MouseEvent) => { setMousePos({ x: e.clientX, y: e.clientY }); }, []);
4. 修正初始化逻辑的依赖
确保气泡初始化仅执行一次,且鼠标事件监听绑定稳定的处理函数:
useEffect(() => { const canvas = canvasRef.current as HTMLCanvasElement; const context = canvas.getContext("2d") as CanvasRenderingContext2D; window.addEventListener("mousemove", handleMouseMove); const initBubbles = []; for (let i = 0; i < 1; i++) { initBubbles.push( new Bubble( randomNumber(0 + BUBBLE_RADIUS, window.innerWidth - BUBBLE_RADIUS), randomNumber(0 + BUBBLE_RADIUS, window.innerHeight - BUBBLE_RADIUS), randomNumber(-1, 4), randomNumber(-4, 4), context ) ); } setBubbles(initBubbles); return () => { window.removeEventListener("mousemove", handleMouseMove); }; }, [handleMouseMove]);
核心优化总结
- 用
useCallback稳定频繁创建的函数,避免触发不必要的useEffect执行。 - 将canvas尺寸调整从渲染循环中剥离,放到resize事件单独处理。
- 确保始终只有一个
requestAnimationFrame循环在运行,避免并行渲染导致的速度异常。
内容的提问来源于stack exchange,提问作者needhelpplease123
相关产品推荐
相关产品推荐

