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正确接收和执行,无法在组件卸载或依赖变化时清理旧的监听器。
解决方案
- 让
draw函数返回清理函数,在Canvas组件的useEffect中接收并返回这个清理函数,确保依赖变化或组件卸载时能清理旧的监听器。 - 每次
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
相关产品推荐
相关产品推荐

