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

React传递state变量给画布绘制函数后鼠标事件失效的解决方法

问题原因

当你把state变量传递给paintCanvas函数时,会触发以下问题:

  1. 组件每次因state更新重新渲染时,paintCanvas函数都会被重新创建(它定义在组件内部,依赖state参数)。
  2. 你的useEffect依赖数组包含了paintCanvas,所以每次函数重新创建,useEffect都会执行并调用paintCanvas(state)。
  3. paintCanvas内部每次被调用时,都会重复给canvas绑定mousedown、mousemove等鼠标事件,同时事件处理函数(如startDrawing、draw)会因闭包捕获旧状态,或重复绑定导致事件触发逻辑混乱,最终鼠标事件失效。
解决办法

核心思路是分离事件绑定逻辑与画布绘制逻辑,避免因state变化反复重置事件监听:

方案1:独立处理事件绑定,仅在初始化时绑定一次

将鼠标事件的绑定/解绑逻辑放在单独的useEffect中,只在canvas元素初始化时执行一次,与state变化解耦;paintCanvas仅负责画布内容的绘制,在state更新时触发重绘即可。

示例代码:

import { useState, useEffect, useRef, useCallback } from 'react';

function App() {
  const canvasRef = useRef(null);
  const [editorText, setEditorText] = useState('');

  // 仅负责绘制画布内容(网格、文本)
  const paintCanvas = useCallback((text) => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const size = 20;
    const width = canvas.width;
    const height = canvas.height;

    // 清空画布
    ctx.clearRect(0, 0, width, height);
    
    // 绘制网格
    ctx.strokeStyle = '#eee';
    for (let i = 0; i <= width; i += size) {
      ctx.beginPath();
      ctx.moveTo(i, 0);
      ctx.lineTo(i, height);
      ctx.stroke();
    }
    for (let i = 0; i <= height; i += size) {
      ctx.beginPath();
      ctx.moveTo(0, i);
      ctx.lineTo(width, i);
      ctx.stroke();
    }

    // 绘制传入的文本
    if (text) {
      ctx.fillStyle = '#333';
      ctx.font = '16px Arial';
      ctx.fillText(text, 10, 30);
    }
  }, []);

  // 独立处理鼠标事件绑定,仅在canvasRef初始化时执行
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    let isDrawing = false;
    let lastX = 0;
    let lastY = 0;

    const startDrawing = (e) => {
      isDrawing = true;
      [lastX, lastY] = [e.offsetX, e.offsetY];
    };

    const draw = (e) => {
      if (!isDrawing) return;
      const ctx = canvas.getContext('2d');
      ctx.strokeStyle = '#000';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(lastX, lastY);
      ctx.lineTo(e.offsetX, e.offsetY);
      ctx.stroke();
      [lastX, lastY] = [e.offsetX, e.offsetY];
    };

    const stopDrawing = () => {
      isDrawing = false;
    };

    // 绑定事件
    canvas.addEventListener('mousedown', startDrawing);
    canvas.addEventListener('mousemove', draw);
    canvas.addEventListener('mouseup', stopDrawing);
    canvas.addEventListener('mouseout', stopDrawing);

    // 组件卸载时解绑事件
    return () => {
      canvas.removeEventListener('mousedown', startDrawing);
      canvas.removeEventListener('mousemove', draw);
      canvas.removeEventListener('mouseup', stopDrawing);
      canvas.removeEventListener('mouseout', stopDrawing);
    };
  }, [canvasRef]);

  // 当editorText变化时,重绘画布
  useEffect(() => {
    paintCanvas(editorText);
  }, [editorText, paintCanvas]);

  return (
    <div>
      <canvas ref={canvasRef} width={600} height={400} style={{ border: '1px solid #000' }} />
      <textarea
        value={editorText}
        onChange={(e) => setEditorText(e.target.value)}
        style={{ width: '600px', height: '100px', marginTop: '10px' }}
      />
    </div>
  );
}

export default App;

方案2:用useCallback缓存paintCanvas函数,避免重复绑定事件

如果必须在paintCanvas中处理事件绑定,可使用useCallback缓存函数引用,同时在绑定事件前先移除旧监听,避免重复绑定:

示例代码:

const paintCanvas = useCallback((text) => {
  const canvas = canvasRef.current;
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  
  // 绘制网格和文本逻辑...

  // 先移除旧的事件监听,再绑定新的
  canvas.removeEventListener('mousedown', startDrawing);
  canvas.removeEventListener('mousemove', draw);
  canvas.removeEventListener('mouseup', stopDrawing);
  canvas.removeEventListener('mouseout', stopDrawing);

  function startDrawing(e) {
    isDrawing = true;
    [lastX, lastY] = [e.offsetX, e.offsetY];
  }

  function draw(e) {
    if (!isDrawing) return;
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(e.offsetX, e.offsetY);
    ctx.stroke();
    [lastX, lastY] = [e.offsetX, e.offsetY];
  }

  function stopDrawing() {
    isDrawing = false;
  }

  // 绑定新事件
  canvas.addEventListener('mousedown', startDrawing);
  canvas.addEventListener('mousemove', draw);
  canvas.addEventListener('mouseup', stopDrawing);
  canvas.addEventListener('mouseout', stopDrawing);
}, [editorText]); // 仅当editorText变化时重新创建函数

这种方案需要注意每次绑定前移除旧监听,否则会导致事件多次触发。

内容的提问来源于stack exchange,提问作者A.G.Progm.Enthusiast

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 03:50:33