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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 05:01:04