React 中的 SVG 平移会导致奇怪的行为

问题描述

下面的 React 组件应该允许平移 SVG。确实如此,但由于某种原因,SVG 的移动会呈指数级加速,因此鼠标指针的几个像素移动会导致 SVG(或者更确切地说,SVG viewBox)的移动越来越大。例如,轻轻拖动鼠标,圆圈就会从屏幕上拉开。

这是一个小提琴:https://jsfiddle.net/bupham/ax473r52/4/

似乎可能发生了一些 React 反馈循环,但我不确定。平移行为代码来自另一个 SO 帖子 here

我尝试将方法调用移动到容器或 SVG,但它仍然发生。我试过将一个函数传递给 setState,仍然发生。我尝试制作 state.viewBox 的浅拷贝而不是制作浅拷贝——仍然会发生。我做错了什么?

export default class SVGContainer extends React.Component {
  constructor(props) {
    super(props);

    this.state= {
      viewBox: {x:0,y:0,w:500,h:500},svgSize: {w: 500,h: 500},scale: 1,isPanning: false,startPoint: {x:0,y:0},endPoint: {x:0,}
  }

  handleMouseDown = (e) => {
    console.log('handleMouseDown e',e)
    this.setState({
      isPanning: true,startPoint: {x: e.clientX,y: e.clientY},})
  }

  handleMouseMove = (e) => {
      this.setState((prevstate,props) => {
        if (prevstate.isPanning) {
          console.log('handleMouseMove e',e.clientX,e.clientY)
          
          let startPoint = prevstate.startPoint;
          const scale = prevstate.scale; 
          const viewBox = prevstate.viewBox;
          const endPoint = {x: e.clientX,y: e.clientY};
          const dx = (startPoint.x - endPoint.x) / scale;
          const dy = (startPoint.y - endPoint.y) / scale;
          const newViewBox = {x:viewBox.x+dx,y:viewBox.y+dy,w:viewBox.w,h:viewBox.h};
          console.log('the view Box',newViewBox)
          return {viewBox: newViewBox};
        }
      }) 
  }

  handleMouseUp = (e) => {
    if (this.state.isPanning){ 
      let startPoint = this.state.startPoint;
      const scale = this.state.scale; 
      const viewBox = this.state.viewBox;
      const endPoint = {x: e.clientX,y: e.clientY};
      var dx = (startPoint.x - endPoint.x)/scale;
      var dy = (startPoint.y - endPoint.y)/scale;
      const endViewBox = {x: viewBox.x+dx,y: viewBox.y+dy,w: viewBox.w,h: viewBox.h};
      console.log('viewBox at mouseup',endViewBox)
      this.setState({
        viewBox: endViewBox,});
      
    }
  }

  handleMouseLeave = (e) => {
    this.setState({
      isPanning: false,})
  }

  render() {

    return (
      <div className="container" >
        <svg width="500" height="500" 
          onWheel={this.handleWheelZoom}
          onMouseDown={this.handleMouseDown}
          onMouseMove={this.handleMouseMove}
          onmouseup={this.handleMouseUp}
          onMouseLeave={this.handleMouseLeave}          
          viewBox={`${this.state.viewBox.x} ${this.state.viewBox.y} ${this.state.viewBox.w} ${this.state.viewBox.h}`}>
          <circle cx="50" cy="50" r="50" /> 
        </svg>
      </div>
    );  
  }
}

解决方法

这在技术上不是一个答案,而是一个解决方案:我决定使用 d3 (4.13.0) 代替,因为 zoom 的跨浏览器/触摸复杂性太多了,无法手动书写。

至于为什么它不起作用,它很可能与 React 和 React 状态的异步性质有关。一位朋友建议使用 requestAnimationFrame() 和/或限制鼠标事件。

这是我使用的相关代码的样子。我为我需要使用 D3 操作的两个 DOM 节点添加了两个 React 引用:

export default class SVGContainer extends React.Component {
  constructor(props) {
    super(props);
    this.svgRef = React.createRef();
    this.gRef = React.createRef();

    this.state= {
      container: {width: 1000,height: 1000},}
  }

  componentDidMount() {
    window.addEventListener('resize',this.handleWindowResize);
    const width = window.innerWidth;
    const height = window.innerHeight;
    this.setState({
      container: {width,height},})

    const svg = d3.select(this.svgRef.current);
    // D3 wants you to call zoom on a container and then apply the zoom transformations
    // elsewhere...you can read why in the docs. 
    svg.call(this.handleZoom).call(this.handleZoom.transform,initialZoom);
  }
  
  componentWillUnmount() {
    window.removeEventListener('resize',this.handleWindowResize);
  }

  handleWindowResize = (e) => {
    const height = this.svgRef.current.clientHeight;
    const width = this.svgRef.current.clientWidth;

    this.setState({
      container: {width,});    
  }

  handleZoom = d3.zoom().on('zoom',e => {
      const g = d3.select(this.gRef.current);
      g.attr('transform',d3.event.transform)
  })

  render() {

    return (
      <div className="SVGContainer">
        <svg 
          width={this.state.container.width} height={this.state.container.height} 
          ref={this.svgRef}>
          <circle cx="50" cy="50" r="50" ref={this.gRef} /> 
        </svg>
      </div>
    );  
  }
}