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

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>
  );
}

问题分析与修复方案

核心问题

  1. React状态更新的异步性:handleMoveEvent中依赖startingPoint状态更新lineCoords,但setStartingPoint是异步操作,拖动时可能拿到旧的起点值,导致线条位置错误。
  2. 状态驱动实时更新的延迟:依赖lineCoords状态变化触发useEffect更新临时线条,状态更新的调度无法跟上鼠标移动的高频事件,导致实时性差。
  3. 全局初始对象的引用问题:全局的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 08:37:04