React Canvas烟花开发中MouseMove坐标无法被init读取问题
问题根因
你遇到的是React Hooks典型的闭包陈旧值问题:
- 你的
useEffect传入了空依赖数组,只会在组件首次挂载时执行1次,内部定义的init、animate函数、以及setInterval的回调,全部捕获的是首次渲染时的上下文,此时MousePosition就是初始值{left: 0, top: 0},后续state更新不会同步到这些已经创建的闭包里。 mousemove回调里能打印出正确坐标,是因为每次组件重渲染都会重新创建handleMouseMove函数,它能拿到当前渲染周期的最新state,但useEffect里的定时任务从创建开始就一直持有旧的state引用。- 另外你直接在组件函数顶层声明
Balls数组、preloaderEffect变量也有隐患:组件每次重渲染都会重置这两个值,会导致已生成的烟花粒子丢失。
修复方法
- 把不需要触发视图更新的可变数据(鼠标实时坐标、粒子数组、效果开关、定时器/动画帧ID)全部存入
useRef,ref的.current属性可以被任意闭包读取到最新值,且修改不会触发组件重渲染,非常适合存canvas动画这类高频更新的状态 - 鼠标移动时直接更新ref存储的坐标,不需要依赖state跨闭包传值
- 组件卸载时清除定时器、动画帧任务,避免内存泄漏
修正后完整代码
import { React, useEffect, useRef, useState } from 'react' export default function Canvas() { const canvasRef = useRef(null); // 用ref存高频更新的可变值,避免闭包陈旧问题 const MousePositionRef = useRef({ left: 0, top: 0 }); const BallsRef = useRef([]); const preloaderEffectRef = useRef(true); const timerRef = useRef(null); const rafRef = useRef(null); const [canvasOn, setCanvasOn] = useState(true); function handleMouseMove(event) { // 直接更新ref值,所有闭包都能拿到最新坐标 MousePositionRef.current = { left: event.clientX, top: event.clientY }; console.log(MousePositionRef.current.left, MousePositionRef.current.top) } useEffect(() => { const canvas = canvasRef.current; const c = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const color = [ 'rgb(65%, 16%, 16%)', 'rgb(80%, 20%, 20%)', 'rgb(75%, 15%, 15%)', 'rgb(70%, 13%, 13%)', 'rgb(80%, 15%, 15%)', 'rgb(86%, 16%, 16%)', 'rgb(94%, 50%, 50%)', 'rgb(93%, 39%, 39%)' ]; const radius = [5, 6, 7, 8, 9, 10]; class ball { constructor(x, y, colorCode, r) { this.position = { x: x, y: y }; this.color = colorCode; this.r = r; this.velocity = { x: Math.round(Math.random() * 1) * 2 - 1, y: Math.round(Math.random() * 1) * 2 - 1 }; this.speed = { x: 20 * Math.random(), y: 20 * Math.random() }; this.ease = 0.9; }; draw() { c.beginPath(); c.shadowBlur = 30; c.shadowColor = this.color; c.fillStyle = this.color; c.arc(this.position.x, this.position.y, this.r, 0, 2 * Math.PI, false); c.fill(); c.closePath(); c.shadowBlur = 0; }; update() { this.draw(); this.position.x += this.velocity.x * this.speed.x; this.position.y += this.velocity.y * this.speed.y; this.speed.x *= this.ease; this.speed.y *= this.ease; }; }; function animate() { if (preloaderEffectRef.current !== true) return rafRef.current = requestAnimationFrame(animate); c.fillStyle = 'rgba(0,0,0,0.1)'; c.fillRect(0, 0, c.width, c.height); BallsRef.current.forEach((x) => { x.update(); }); }; function init() { timerRef.current = setInterval(() => { if (preloaderEffectRef.current !== true) { return }; var a = Math.round(Math.random() * (color.length - 1)); var b = Math.round(Math.random() * (radius.length - 1)); // 从ref里读取最新的鼠标坐标 const { left, top } = MousePositionRef.current; for (var i = 0; i < 5; i++) { BallsRef.current.push(new ball(left, top, color[a], radius[b])); if (BallsRef.current.length > 100) { BallsRef.current.shift(); }; }; }, 100) }; const stopTimer = setTimeout(() => { preloaderEffectRef.current = false; setCanvasOn(false); // 清除定时器 clearInterval(timerRef.current); cancelAnimationFrame(rafRef.current); }, 5000); animate(); init(); // 组件卸载时清除所有副作用 return () => { clearTimeout(stopTimer); clearInterval(timerRef.current); cancelAnimationFrame(rafRef.current); } }, []) return ( <div> {canvasOn && <canvas ref={canvasRef} style={{ background: 'black' }} onMouseMove={handleMouseMove}></canvas>} </div> ) }
内容的提问来源于stack exchange,提问作者TomFan
相关产品推荐
相关产品推荐

