React中useEffect无法更新two.js绘图应用线条宽度的问题
问题分析
- 闭包变量捕获失效:
val是普通变量,drag函数在组件初始化时创建,闭包捕获的是初始状态的val(未定义),后续useEffect更新val后,drag函数仍读取旧值。 - Two.js实例重复初始化:
var two = new Two(...)直接写在组件顶层,每次组件渲染都会生成新的Two实例,导致绘图上下文混乱。 - 线条宽度更新时机错误:
useEffect中判断line是否存在,但line是鼠标按下后才创建的,初始状态下line为null,导致useEffect首次执行时不会更新val。 - 异步状态日志误导:
handleDraw里的console.log(draw)打印的是更新前的旧值,因为setState是异步操作。
修复方案
1. 用React状态管理线条宽度
将线条宽度改为React状态,避免闭包捕获旧值的问题,同时支持实时更新。
2. 正确初始化Two.js实例
将Two实例的创建放入useEffect,并用useRef保存实例引用,确保只初始化一次。
3. 优化事件绑定与变量存储
用useRef存储临时变量(如line、mouse),将鼠标事件绑定到useEffect中,依赖线条宽度状态实现实时更新。
完整修复代码
import React, { useRef, useEffect, useState } from 'react' import { Link } from "react-router-dom"; import Two from "two.js"; import './paintApp.css'; function PaintApp() { const ref = useRef(null); const twoRef = useRef(null); // 保存Two实例 const [draw, setDraw] = useState(false); const [lineWidth, setLineWidth] = useState(10); // 用状态管理线条宽度 // 初始化Two.js实例 useEffect(() => { if (!twoRef.current) { const two = new Two({ type: Two.Types.svg, width: 900, height: 500, autostart: true }); two.appendTo(ref.current); twoRef.current = two; } }, []); // 切换绘图/擦除模式时更新线条宽度 useEffect(() => { const newWidth = draw ? 0 : 10; setLineWidth(newWidth); // 更新已有线条的宽度 if (twoRef.current) { twoRef.current.scene.children.forEach(child => { if (child.linewidth !== undefined) { child.linewidth = newWidth; } }); twoRef.current.update(); } }, [draw]); const handleDraw = () => { setDraw(prev => !prev); }; // 用ref存储临时变量,避免闭包问题 const lineRef = useRef(null); const mouseRef = useRef(new Two.Vector()); // 鼠标/触摸事件绑定到useEffect useEffect(() => { const two = twoRef.current; if (!two) return; const handleMouseDown = (e) => { mouseRef.current.set(e.offsetX, e.offsetY); lineRef.current = null; window.addEventListener('mousemove', handleDrag); window.addEventListener('mouseup', handleDragEnd); }; const handleDrag = (e) => { const x = e.offsetX; const y = e.offsetY; if (!lineRef.current) { const v1 = makePoint(mouseRef.current); const v2 = makePoint(x, y); lineRef.current = two.makeCurve([v1, v2], true); lineRef.current.noFill().stroke = '#333'; // 直接读取最新的线条宽度状态 lineRef.current.linewidth = lineWidth; lineRef.current.vertices.forEach(v => { v.addSelf(lineRef.current.translation); }); lineRef.current.translation.clear(); } else { const v1 = makePoint(x, y); lineRef.current.vertices.push(v1); } mouseRef.current.set(x, y); }; const handleDragEnd = () => { window.removeEventListener('mousemove', handleDrag); window.removeEventListener('mouseup', handleDragEnd); }; const handleTouchStart = (e) => { e.preventDefault(); const touch = e.changedTouches[0]; mouseRef.current.set(touch.pageX, touch.pageY); lineRef.current = null; window.addEventListener('touchmove', handleTouchDrag); window.addEventListener('touchend', handleTouchEnd); }; const handleTouchDrag = (e) => { e.preventDefault(); const touch = e.changedTouches[0]; handleDrag({ offsetX: touch.pageX, offsetY: touch.pageY }); }; const handleTouchEnd = (e) => { e.preventDefault(); window.removeEventListener('touchmove', handleTouchDrag); window.removeEventListener('touchend', handleTouchEnd); }; window.addEventListener('mousedown', handleMouseDown); window.addEventListener('touchstart', handleTouchStart); // 组件卸载时清理事件监听 return () => { window.removeEventListener('mousedown', handleMouseDown); window.removeEventListener('touchstart', handleTouchStart); window.removeEventListener('mousemove', handleDrag); window.removeEventListener('mouseup', handleDragEnd); window.removeEventListener('touchmove', handleTouchDrag); window.removeEventListener('touchend', handleTouchEnd); }; }, [lineWidth]); twoRef.current?.bind('update', update); function update(frameCount, timeDelta) { const two = twoRef.current; if (!two) return; for (let i = 0; i < two.scene.children.length; i++) { const child = two.scene.children[i]; for (let j = 0; j < child.vertices.length; j++) { const v = child.vertices[j]; if (!v.position) return; v.x = v.position.x; v.y = v.position.y; } } } function makePoint(x, y) { if (arguments.length <= 1) { y = x.y; x = x.x; } const v = new Two.Anchor(x, y); v.position = new Two.Vector().copy(v); return v; } return ( <div> <div>paintApp</div> <div><Link to='/application'>Back</Link></div> <button onClick={handleDraw}>{draw ? '切换到绘图' : '切换到擦除'}</button> <div ref={ref} id="picture"></div> </div> ) } export default PaintApp
关键修复说明
useRef存储实例与临时变量:避免组件渲染时重复创建Two实例,同时解决闭包中变量捕获旧值的问题。- 状态驱动线条宽度:用
lineWidth状态管理宽度值,确保drag函数能读取到最新值。 - 事件绑定依赖状态:鼠标事件绑定依赖
lineWidth,宽度变化时重新绑定事件,保证闭包捕获最新状态。 - 批量更新已有线条:切换模式时同步更新画布上所有已绘制线条的宽度,实现模式切换的即时生效。
内容的提问来源于stack exchange,提问作者RadhoFan
相关产品推荐
相关产品推荐

