简体   繁体   English

子组件问题的多次重新渲染

[英]Multiple re-rendering of child component problem

I have a child component called Plot.js.我有一个名为 Plot.js 的子组件。 This takes a prop plot which is an object with a property points , which contains an array of x, y points.这需要一个道具plot ,它是一个 object 属性points ,它包含一个 x,y 点数组。 I also have an array of files .我还有一组files files are looped through, and for each file a polygon is drawn on a canvas according to the points in plot.gate.points :循环遍历files ,并根据 plot.gate.points 中的点在plot.gate.points上绘制每个文件的多边形:

在此处输入图像描述

The Parent component looks like this:父组件如下所示:

class Parent extends React.Component {
    constructor(props) {
      super(props)
      this.state = { 
        plots: plots.plotList,
        files: files.fileList
      }
      
      this.onEditGate = this.onEditGate.bind(this);
    }
  
    onEditGate = (change) => {
      
        this.state.plots[change.plotIndex] = change.plot;

        console.log("setting the state");
        this.setState({
          files: files,
          plots: this.state.plots
        });
    };
  
  render() {
    return (
          <table className="workspace">
                <tbody>
                  {files.fileList.map((file, fileIndex) => { 
                    return (
                      <tr key={`tr-${fileIndex}`}>
                        {plots.plotList.map((plot, plotIindex) => {
                          return (
                            <td key={`td-${plotIindex}`}>

                              <Plot 
                                plotIndex={`${fileIndex}-${plotIindex}`} 
                                plot={plot} 
                                file={file}
                                onEditGate={this.onEditGate}
                                ></Plot>
                            </td>
                        );
                    })}
              </tr>
            );
          })}
                </tbody>
      </table>
    );
  }
}

files and plots are: filesplots是:

let plots = JSON.parse('{"plotList":[{"population":"All","gate":{"points":[[10,10],[70,10],[70,70],[10,70]]}}]}');
let files = JSON.parse('{"fileList":[{"file":"a"},{"file":"b"}]}');

And Plot is:而 Plot 是:

function Plot(props) {
  console.log('in function Plot, plotIndex is ' + props.plotIndex + ', props.plot points are ',         props.plot.gate.points);
  
  const [localPlot, setLocalPlot] = React.useState(props.plot);
  
  React.useEffect(() => {
    setLocalPlot(props.plot);
    
    console.log('in useEffect, plotIndex is ' + props.plotIndex + ', localPlot points are ', localPlot.gate.points);
    
    const context = getContext(props.plotIndex);
    context.clearRect(0, 0, 200, 200);
    context.fillStyle = "white";
    
    drawPolygonLine(context, localPlot);
    
    
   }, [localPlot, props.plot, props.file]);
  
  
  const drawPolygonLine = (context, plot) => {

    context.strokeStyle = "red";
    context.lineWidth = 1;
    context.beginPath();

    // draw the first point of the gate
    context.moveTo(plot.gate.points[0][0], plot.gate.points[0][1]);

    plot.gate.points.forEach((pointOnCanvas) => {
      context.lineTo(pointOnCanvas[0], pointOnCanvas[1]);
    });

    context.closePath();
    context.stroke();
  };
  
  
  const movePolygon = (plotIndex) => {

    localPlot.gate.points = props.plot.gate.points.map((point) => {
      return [point[0] + 40, point[1] + 40];
    });

    setLocalPlot(JSON.parse(JSON.stringify(localPlot)));
    
    let change = {
      type: "EditPolygon",
      plot: localPlot,
      plotIndex: props.plotIndex.split("-")[1],
      points: JSON.parse(JSON.stringify(localPlot.gate.points)),
    };

    props.onEditPolygon(change);
  }
  
  return (
    <div>
        <canvas
          style={{ border: "thick solid #32a1ce" }}
          className="canvas"
          id={`canvas-${props.plotIndex}`}
          width={200}
          height={200}
          
        />
         <button
          onClick={() => movePolygon(props.plotIndex)}
          >
          Move plot {props.plotIndex}
        </button>
      </div>
  );
}

I need to make a copy of plot (called localPlot ).我需要复制plot (称为localPlot )。 This is because a user can manipulate a polygon on one of the plot. As the mouse moves, I get the new points and then do setLocalPlot(JSON.parse(JSON.stringify(localPlot)));这是因为用户可以在 plot 之一上操作多边形。当鼠标移动时,我得到新的点,然后执行setLocalPlot(JSON.parse(JSON.stringify(localPlot))); . . This re-renders JUST THE INSTANCE of the plot i'm manipulating.这仅重新呈现我正在操作的 plot 的实例。 On mouse up, I propagate the final polygon points back up to the State, and re-render everything, looping through files and creating the plots with the new polygon points.在鼠标向上时,我将最终的多边形点传播回 State,并重新渲染所有内容,遍历文件并使用新的多边形点创建绘图。 So the plots will all look identical.所以这些图看起来都一样。

This all works fine.这一切都很好。 I have created a simplified CodePen.我创建了一个简化的 CodePen。 Omitted is the re-render of JUST THE PLOT instance being manipulated on mouse move (as this works fine).省略了在鼠标移动时操作的仅 PLOT 实例的重新渲染(因为这工作正常)。 Instead, I've added a "Move" button to show what happens when I propagate back up to the State. Code pen is here: https://codepen.io/mark-kelly-the-looper/pen/WNdrRBj?editors=0011相反,我添加了一个“移动”按钮来显示当我传播回 State 时会发生什么。代码笔在这里: https://codepen.io/mark-kelly-the-looper/pen/WNdrRBj?editors =0011

However, when I look at the console.log outputs, I see that after updating the State, each Plot component instance re-renders 3 times, not just once as I expected, see code pen logs:但是,当我查看 console.log 输出时,我看到在更新 State 之后,每个 Plot 组件实例重新呈现 3 次,而不是我预期的一次,请参阅代码笔日志:

在此处输入图像描述

The problem is my app does a huge amount of work within useEffect() , calculating the position of thousands of x, y points, as well as the position of the polygon.问题是我的应用程序在useEffect()中做了大量工作,计算了数千个 x、y 点的 position,以及多边形的 position。 Therefore, performance is very important.因此,性能非常重要。 Why is each instance re-rendering 3 times?为什么每个实例重新渲染 3 次? Is there a better approach?有更好的方法吗?

EDIT: The reason I have localPlot is as a user drags, I need to re-render just the instance of the Plot AND i need useEffect() to be trigger, because within useEffect() is where all the drawing on the canvas takes place.编辑:我有localPlot的原因是用户拖动,我需要重新渲染 Plot 的实例,我需要useEffect()来触发,因为useEffect()是 canvas 上所有绘图发生的地方. If theres another way to show the polygon being dragged, then Im all ears...如果有另一种方式来显示被拖动的多边形,那么我洗耳恭听......

EDIT I have added a more comprehensive Code Pen here where you can see the re-rendering of a Plot instance on dragging of the polygon.编辑我在这里添加了一个更全面的代码笔,您可以在其中看到在拖动多边形时重新渲染 Plot 实例。 After dragging the polygon and on mouse up, you can see each instance of the Plot component reloads 3 times instead of once:拖动多边形并在鼠标上移后,您可以看到 Plot 组件的每个实例重新加载 3 次而不是一次:

在此处输入图像描述

As mentioned above, this is not acceptable for my application as within useEffect() of each Plot instance I do thousands of calculations (not included here).如上所述,这对我的应用程序来说是不可接受的,因为在每个Plot实例的useEffect()中我进行了数千次计算(此处未包括在内)。 Im beginning to think that, fundamentally, this is a limitation of React itself?我开始认为,从根本上说,这是 React 本身的局限性? Code Pen is here: https://codepen.io/mark-kelly-the-looper/pen/bGYzZQR?editors=0011 .代码笔在这里: https://codepen.io/mark-kelly-the-looper/pen/bGYzZQR?editors=0011

You are setting localPlot in the effect that depends on localPlot, so it's always going to render at least twice.您在依赖于 localPlot 的效果中设置 localPlot,因此它总是至少要渲染两次。 Usually that's an infinite loop.通常这是一个无限循环。

You also have additional dependencies in this effect that are not used.您还具有未使用的此效果的其他依赖项。 Removing them removes unnecessary renders.删除它们会删除不必要的渲染。

React.useEffect(() => {
    console.log('in useEffect', Date.now());
    
    const context = getContext(props.plotIndex);
    context.clearRect(0, 0, 200, 200);
    context.fillStyle = "white";
    
    drawGateLine(context, localPlot);
   }, [localPlot]);

Okay I went through the spiderweb of renders and got the drawing effect to run only once for each instance when the propagation occurs.好吧,我浏览了渲染的蜘蛛网,并让绘图效果在传播发生时为每个实例只运行一次。

I had to update the setStates too to remove all the mutation.我也必须更新 setStates 以删除所有突变。

Every comment above is still correct;上面的每个评论仍然是正确的; there is never a need to update an effect's dependency in that effect.永远不需要更新效果对该效果的依赖性。 This is always a mistake.这总是一个错误。

Because of the way the offset state is on the module (so React doesn't know about it), you had to trick React into thinking things had updated, but the flow was very circular.由于偏移量 state 在模块上的方式(因此 React 不知道它),你不得不欺骗 React 认为事情已经更新,但流程非常循环。

Reading the pen will explain better.读笔会更好地解释。 https://codepen.io/windowsill/pen/BaJzRmY https://codepen.io/windowsill/pen/BaJzRmY

When you put localPlot and props.plot in the useEffect's dependency list, React will do a SHALLOW comparison of these variables with their previous values after each render to decide whether to run the code inside the hook.当您将 localPlot 和 props.plot 放入 useEffect 的依赖列表中时,React 将在每次渲染后将这些变量与它们之前的值进行浅层比较,以决定是否运行钩子内的代码。 You should compare the contents of these objects instead of their object reference.您应该比较这些对象的内容,而不是它们的 object 引用。

Also, the useEffect hook is run AFTER each render, not before it, and thus setting state with setState in a useEffect hook will always cause an extra render, so it's better not to put that in there.此外,useEffect 钩子在每次渲染之后运行,而不是在渲染之前运行,因此在 useEffect 钩子中使用 setState 设置 state 总是会导致额外的渲染,所以最好不要把它放在那里。

The solution is to do a deep comparison of localPlot and props.plot on each render, outside of your useEffect hook, and only update localPlot if they are different:解决方案是在 useEffect 挂钩之外的每个渲染器上对 localPlot 和 props.plot 进行深入比较,并且仅在它们不同时更新 localPlot:

const plotsAreSame = (plot1, plot2) => {
  if (plot1.population != plot2.population) return false;
  return (plot1.gate.points.length===plot2.gate.points.length && plot1.gate.points.every(
    (p,i)=>(p[0]===plot2.gate.points[i][0] && p[1]===plot2.gate.points[i][1])
  ))
}

function Plot(props) {
  console.log('in function Plot, plotIndex is ' + props.plotIndex + ', props.plot points are ',         props.plot.gate.points);
  
  const [localPlot, setLocalPlot] = React.useState(props.plot);
  if (!plotsAreSame(localPlot, props.plot)) {setLocalPlot(props.plot)}
  
  React.useEffect(() => {
    console.log('in useEffect, plotIndex is ' + props.plotIndex + ', localPlot points are ', localPlot.gate.points);
    
    const context = getContext(props.plotIndex);
    context.clearRect(0, 0, 200, 200);
    context.fillStyle = "white";
    
    drawGateLine(context, localPlot);
    
    
   }, [localPlot, props.plotIndex]);

This way the two components will only be rendered once.这样两个组件只会被渲染一次。

You don't really need the localState state, it just complicates your process.您实际上并不需要localState state,它只会使您的流程复杂化。

Briefly describe the modified steps:简述修改步骤:

  1. You pass plot props from the Parent as coordinate data for the Plot component.您从Parent传递plot道具作为Plot组件的坐标数据。
  2. Through the onEditGate callback function, let the Parent component get the information after the coordinate movement, and then modify the coordinate value.通过onEditGate回调function,让Parent组件获取到坐标移动后的信息,进而修改坐标值。
  • Result: Each Plot component is updated once, and the code becomes cleaner.结果:每个Plot组件更新一次,代码变得更干净。
    在此处输入图像描述 (You can check the full display at the bottom (CodeSandbox).) (您可以查看底部的完整显示(CodeSandbox)。)

Code fragment (modified slightly from your code)代码片段(根据您的代码略作修改)

function Plot({ plotIndex, plot, file, onEditGate = () => {} }) {
  console.log(
    "in function Plot, plotIndex is " + plotIndex + ", props.plot points are ",
    plot.gate.points
  );

  useEffect(() => {
    console.log(
      "in useEffect, plotIndex is " + plotIndex + ", localPlot points are ",
      plot.gate.points
    );

    const context = getContext(plotIndex);
    context.clearRect(0, 0, 200, 200);
    context.fillStyle = "white";

    drawGateLine(context, plot);
  }, [plot, plotIndex]);

  const drawGateLine = (context, plot) => {
    context.strokeStyle = "red";
    context.lineWidth = 1;
    context.beginPath();

    // draw the first point of the gate
    context.moveTo(plot.gate.points[0][0], plot.gate.points[0][1]);

    plot.gate.points.forEach((pointOnCanvas) => {
      context.lineTo(pointOnCanvas[0], pointOnCanvas[1]);
    });

    context.closePath();
    context.stroke();
  };

  const movePolygon = (plotIndex) => {
    const saveCopyLocalPlot = JSON.parse(JSON.stringify(plot));
    const newLocal = saveCopyLocalPlot.gate.points.map((point) => {
      return [point[0] + 40, point[1] + 40];
    });
    saveCopyLocalPlot.gate.points = newLocal;

    let change = {
      type: "EditGate",
      plot: saveCopyLocalPlot,
      plotIndex: plotIndex.split("-")[1],
      points: JSON.parse(JSON.stringify(saveCopyLocalPlot.gate.points))
    };

    onEditGate(change);
  };

  return (
    <div>
      <canvas
        style={{ border: "thick solid #32a1ce" }}
        className="canvas"
        id={`canvas-${plotIndex}`}
        width={200}
        height={200}
      />
      <button onClick={() => movePolygon(plotIndex)}>
        Move plot {plotIndex}
      </button>
    </div>
  );
}

export default Plot;

Full code sample (modified slightly from your code)完整代码示例(根据您的代码略作修改)

编辑 TextField selectionStart

Hope to help you:)希望能帮到你:)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM