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

React拆分useEffect代码后Canvas事件监听器无法移除问题

问题说明

我用React+HTML Canvas开发技术分析网站,支持趋势线、图形、自由绘图等功能,已实现点击按钮切换自由绘图启停的基础原型。出于后续扩展类似TradingView功能的代码组织需求,我想把自由绘图的代码从Canvas组件的useEffect钩子拆到外部函数中,但拆分后出现问题:开启自由绘图后,切换isDrawing为false时,事件监听器无法移除,仍然可以继续绘图。

原因分析

拆分后的代码中,每次调用draw函数时,内部的start、stop、draw1等函数都会被重新创建,形成新的函数引用。当isDrawing从true变为false时,调用draw(context, false)尝试移除监听器,此时用的是新创建的start/stop函数,和之前添加监听器时的旧函数引用不匹配,导致移除失败。另外,draw函数返回的清理函数没有被useEffect正确接收和执行,无法在组件卸载或依赖变化时清理旧的监听器。

解决方案
  1. 让draw函数返回清理函数,在Canvas组件的useEffect中接收并返回这个清理函数,确保依赖变化或组件卸载时能清理旧的监听器。
  2. 每次isDrawing变化时,先清理上一次的监听器,再根据新的状态添加或移除监听器,保证函数引用的一致性。
修改后的代码

Canvas.js

const Canvas = ({ height, width, draw }) => {
  const canvas = React.useRef();
  const isDrawing = useSelector((state) => state.freeDrawBoolean.value);

  React.useEffect(() => {
    const context = canvas.current.getContext("2d");
    // 调用draw函数,获取清理逻辑
    const cleanup = draw(context, isDrawing);
    
    // 返回清理函数,useEffect会在依赖变化或组件卸载时执行
    return cleanup;
  }, [isDrawing, draw]);

  return (
    <canvas
      ref={canvas}
      height={height}
      width={width}
      style={{ border: "1px solid red" }}
    />
  );
};

export default Canvas;

App.js

const draw = (context, isDrawing) => {
  let coord = { x: 0, y: 0 };

  // 定义所有绘图相关函数,确保本次调用中引用唯一
  function reposition(event) {
    coord.x = event.clientX - context.canvas.offsetLeft;
    coord.y = event.clientY - context.canvas.offsetTop;
  }

  function draw1(event) {
    context.beginPath();
    context.lineWidth = 5;
    context.lineCap = "round";
    context.strokeStyle = "#ACD3ED";
    context.moveTo(coord.x, coord.y);
    reposition(event);
    context.lineTo(coord.x, coord.y);
    context.stroke();
  }

  function start(event) {
    document.addEventListener("mousemove", draw1);
    reposition(event);
  }

  function stop() {
    document.removeEventListener("mousemove", draw1);
  }

  // 根据当前isDrawing状态添加/移除监听器
  if (isDrawing) {
    document.addEventListener("mousedown", start);
    document.addEventListener("mouseup", stop);
  } else {
    document.removeEventListener("mousedown", start);
    document.removeEventListener("mouseup", stop);
    // 额外清理mousemove监听器,防止切换时仍处于绘图状态
    document.removeEventListener("mousemove", draw1);
  }

  // 返回清理函数,确保所有监听器都能被移除
  return () => {
    document.removeEventListener("mousedown", start);
    document.removeEventListener("mouseup", stop);
    document.removeEventListener("mousemove", draw1);
  };
};

function App() {
  const isDrawing = useSelector((state) => state.freeDrawBoolean.value);
  const dispatch = useDispatch();

  return (
    <>
      <Canvas height={500} width={500} draw={draw} />
      <button onClick={() => dispatch(changeBool())}>changeBool</button>
      <span>value: {isDrawing ? "true" : "false"}</span>
    </>
  );
}

export default App;
关键改进点
  • 清理函数传递:useEffect接收draw返回的清理函数,确保每次isDrawing变化时,先清理上一次的监听器,再执行新的逻辑。
  • 函数引用一致性:每次调用draw时,内部的start/stop/draw1都是本次调用的唯一引用,添加和移除监听器用的是同一批函数,避免引用不匹配的问题。
  • 额外清理:在isDrawing变为false时,主动清理mousemove监听器,防止用户在绘图过程中切换状态导致监听器残留。

内容的提问来源于stack exchange,提问作者Caleb

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 11:48:20