React+D3拖动更新线条终点时无法实时显示的问题
问题:React + D3 绘制线条时无法实时更新拖动中的线条
我正在使用React和D3绘制线条,目前线条可绘制,但按住鼠标拖动(更改线条终点)时,线条无法实时显示或更新,仅在松开鼠标按钮后才正常显示。以下是我的源代码:
组件代码
import * as d3 from 'd3'; import { useEffect, useRef, useState } from 'react'; let x; let y; const point = { x, y }; const pairOfPoints = { start: point, stop: point }; export default function PointerDraw() { const [lineCoords, setLineCoords] = useState(pairOfPoints); const [lines, setLines] = useState([]); const [startingPoint, setStartingPoint] = useState(point); const isDrawingNow = useRef(false); function handleDownEvent(event) { isDrawingNow.current = true; const currentLocation = d3.pointer(event); setStartingPoint({ x: currentLocation[0], y: currentLocation[1] }); const linesSvg = d3.select('.lines'); linesSvg .append('line') .attr('x1', currentLocation[0]) .attr('y1', currentLocation[1]) .attr('x2', currentLocation[0]) .attr('y2', currentLocation[1]) .attr('id', 'lineInProgress'); } function handleMoveEvent(event) { if (isDrawingNow.current) { const currentLocation = d3.pointer(event); setLineCoords({ start: startingPoint, stop: { x: currentLocation[0], y: currentLocation[1], }, }); } } function handleUpEvent() { isDrawingNow.current = false; // stop drawing setLines([...lines, lineCoords]); } useEffect(() => { const linesSvg = d3.select('.lines'); linesSvg .selectAll('line') .data(lines) .join('line') .attr('x1', (d) => d.start.x) .attr('y1', (d) => d.start.y) .attr('x2', (d) => d.stop.x) .attr('y2', (d) => d.stop.y) .attr('id', '') .attr('stroke', 'blue') .attr('stroke-width', '3px'); }, [lines]); useEffect(() => { const liveLine = d3.select('#lineInProgress'); if (liveLine) { // this is the line being drawn liveLine .attr('x1', lineCoords.start.x) .attr('y1', lineCoords.start.y) .attr('x2', lineCoords.stop.x) .attr('y2', lineCoords.stop.y); } }, [lineCoords]); return ( <g className="lines" onMouseDown={handleDownEvent} onMouseUp={handleUpEvent} onMouseMove={handleMoveEvent} > <rect x={1} y={1} width={800} height={600} fill="lightgrey" /> </g> ); }
组件使用代码
import './App.css'; import PointerDraw from './PointerDraw'; export default function App() { return ( <div className="App"> <h1>Click and drag to draw</h1> <svg width="1920" height="1080"> <PointerDraw /> </svg> </div> ); }
问题分析与修复方案
核心问题
- React状态更新的异步性:
handleMoveEvent中依赖startingPoint状态更新lineCoords,但setStartingPoint是异步操作,拖动时可能拿到旧的起点值,导致线条位置错误。 - 状态驱动实时更新的延迟:依赖
lineCoords状态变化触发useEffect更新临时线条,状态更新的调度无法跟上鼠标移动的高频事件,导致实时性差。 - 全局初始对象的引用问题:全局的
point对象初始值为{x: undefined, y: undefined},可能引发初始状态异常。
修复后的代码
import * as d3 from 'd3'; import { useEffect, useRef, useState } from 'react'; export default function PointerDraw() { const [lines, setLines] = useState([]); const isDrawingNow = useRef(false); const startPointRef = useRef({ x: 0, y: 0 }); const tempLineRef = useRef(null); function handleDownEvent(event) { isDrawingNow.current = true; const currentLocation = d3.pointer(event); startPointRef.current = { x: currentLocation[0], y: currentLocation[1] }; // 创建临时线条并存储引用 const linesSvg = d3.select('.lines'); tempLineRef.current = linesSvg .append('line') .attr('x1', currentLocation[0]) .attr('y1', currentLocation[1]) .attr('x2', currentLocation[0]) .attr('y2', currentLocation[1]) .attr('stroke', 'blue') .attr('stroke-width', '3px') .node(); // 获取DOM节点引用 } function handleMoveEvent(event) { if (isDrawingNow.current && tempLineRef.current) { const currentLocation = d3.pointer(event); // 直接操作DOM更新临时线条,无需依赖React状态 d3.select(tempLineRef.current) .attr('x2', currentLocation[0]) .attr('y2', currentLocation[1]); } } function handleUpEvent() { if (!isDrawingNow.current || !tempLineRef.current) return; isDrawingNow.current = false; // 获取临时线条的最终坐标 const line = { start: { ...startPointRef.current }, stop: { x: +tempLineRef.current.getAttribute('x2'), y: +tempLineRef.current.getAttribute('y2') } }; setLines(prev => [...prev, line]); // 移除临时线条的标识并清空引用 d3.select(tempLineRef.current).attr('id', ''); tempLineRef.current = null; } useEffect(() => { const linesSvg = d3.select('.lines'); linesSvg .selectAll('line.drawn-line') .data(lines) .join('line') .attr('class', 'drawn-line') .attr('x1', d => d.start.x) .attr('y1', d => d.start.y) .attr('x2', d => d.stop.x) .attr('y2', d => d.stop.y) .attr('stroke', 'blue') .attr('stroke-width', '3px'); }, [lines]); return ( <g className="lines" onMouseDown={handleDownEvent} onMouseUp={handleUpEvent} onMouseMove={handleMoveEvent} > <rect x={1} y={1} width={800} height={600} fill="lightgrey" /> </g> ); }
关键改动说明
- 用Ref存储临时数据:使用
startPointRef存储起点,tempLineRef存储临时线条的DOM节点,避免依赖异步的React状态,保证数据实时性。 - 直接操作DOM更新临时线条:在
handleMoveEvent中直接通过D3操作临时线条的DOM节点,跳过React状态更新的调度,确保鼠标拖动时线条实时跟随。 - 优化状态更新逻辑:松开鼠标时,从DOM节点获取最终坐标更新
lines状态,避免状态不一致问题。 - 移除全局初始对象:删除全局的
point和pairOfPoints,避免引用类型导致的状态异常。
内容的提问来源于stack exchange,提问作者Velusamy Velu
相关产品推荐
相关产品推荐

