如何使用useMemo或useCallback防止在React中不必要的重新渲染?

时间:2020-05-02 15:42:13

标签: javascript reactjs

我正在尝试使用react钩子重新创建dijkstras寻路可视化工具。

包装器组件如下

import React, { useState, useEffect, useCallback, useRef } from "react";
import Node from "../Node/Node";

import "./PathfindingVisualizer.css";

import { dijkstra, getNodesInShortestPathOrder } from "../algorithms/dijkstras";

const START_NODE_ROW = 0;
const START_NODE_COL = 0;
const FINISH_NODE_ROW = 0;
const FINISH_NODE_COL = 3;

const TOTAL_ROWS = 3;
const TOTAL_COLS = 6;

const PathfindingVisualizer = () => {
  const [nodeGrid, setNodeGrid] = useState({
    grid: []
  });

  const mouseIsPressed = useRef(false);

  useEffect(() => {
    const grid1 = getInitialGrid();
    setNodeGrid({ ...nodeGrid, grid: grid1 });
  }, []);

  const handleMouseDown = useCallback((row, col) => {
    //console.log(newGrid);
    setNodeGrid(prevGrid => ({
      grid: getNewGridWithWallToggled(prevGrid.grid, row, col)
    }));
    mouseIsPressed.current = true;
    //console.log(nodeGrid);
  }, []);

  // function handleMouseDown(row, col) {
  //   const newGrid = getNewGridWithWallToggled(nodeGrid.grid, row, col);
  //  console.log(newGrid);
  //   setNodeGrid({...nodeGrid, nodeGrid[row][col]= newGrid});
  // }

  const handleMouseEnter = useCallback((row, col) => {
    //console.log(mouseIsPressed);
    if (mouseIsPressed.current) {
      setNodeGrid(prevNodeGrid => ({
        ...prevNodeGrid,
        grid: getNewGridWithWallToggled(prevNodeGrid.grid, row, col)
      }));
    }
  }, []);

  const handleMouseUp = useCallback(() => {
    mouseIsPressed.current = false;
  }, []);

  const animateDijkstra = (visitedNodesInOrder, nodesInShortestPathOrder) => {
    for (let i = 0; i <= visitedNodesInOrder.length; i++) {
      if (i === visitedNodesInOrder.length) {
        setTimeout(() => {
          animateShortestPath(nodesInShortestPathOrder);
        }, 10 * i);
        return;
      }
      setTimeout(() => {
        const node = visitedNodesInOrder[i];
        document.getElementById(`node-${node.row}-${node.col}`).className =
          "node node-visited";
      }, 10 * i);
    }
  };

  const animateShortestPath = nodesInShortestPathOrder => {
    for (let i = 0; i < nodesInShortestPathOrder.length; i++) {
      setTimeout(() => {
        const node = nodesInShortestPathOrder[i];
        document.getElementById(`node-${node.row}-${node.col}`).className =
          "node node-shortest-path";
      }, 50 * i);
    }
  };

  const visualizeDijkstra = () => {
    const grid = nodeGrid.grid;
    console.log(grid);
    const startNode = grid[START_NODE_ROW][START_NODE_COL];
    const finishNode = grid[FINISH_NODE_ROW][FINISH_NODE_COL];
    const visitedNodesInOrder = dijkstra(grid, startNode, finishNode);
    const nodesInShortestPathOrder = getNodesInShortestPathOrder(finishNode);
    animateDijkstra(visitedNodesInOrder, nodesInShortestPathOrder);
  };

  //console.log(nodeGrid.grid);
  //console.log(visualizeDijkstra());
  return (
    <>
      <button onClick={visualizeDijkstra}>
        Visualize Dijkstra´s Algorithm
      </button>
      <div className="grid">
        test
        {nodeGrid.grid.map((row, rowIdx) => {
          return (
            <div className="row" key={rowIdx}>
              {row.map((node, nodeIdx) => {
                const { row, col, isStart, isFinish, isWall } = node;
                return (
                  <Node
                    key={nodeIdx}
                    col={col}
                    row={row}
                    isStart={isStart}
                    isFinish={isFinish}
                    isWall={isWall}
                    onMouseDown={handleMouseDown}
                    onMouseEnter={handleMouseEnter}
                    onMouseUp={handleMouseUp}
                  />
                );
              })}
            </div>
          );
        })}
      </div>
    </>
  );
};

export default PathfindingVisualizer;

//----------------------------------------------------------

const getInitialGrid = () => {
  const grid = [];
  for (let row = 0; row < TOTAL_ROWS; row++) {
    const currentRow = [];
    for (let col = 0; col < TOTAL_COLS; col++) {
      currentRow.push(createNode(col, row));
    }
    grid.push(currentRow);
  }
  return grid;
};

const createNode = (col, row) => {
  return {
    col,
    row,
    isStart: row === START_NODE_ROW && col === START_NODE_COL,
    isFinish: row === FINISH_NODE_ROW && col === FINISH_NODE_COL,
    distance: Infinity,
    isVisited: false,
    isWall: false,
    previousNode: null
  };
};

const getNewGridWithWallToggled = (grid, row, col) => {
  const newGrid = grid.slice();
  const node = newGrid[row][col];
  const newNode = {
    ...node,
    isWall: !node.isWall
  };
  newGrid[row][col] = newNode;
  return newGrid;
};

我的代码和框:https://codesandbox.io/s/twilight-bird-2f8hc?file=/src/PathfindingVisualizer/PathfindingVisualizer.jsx

视频教程供参考:https://www.youtube.com/watch?v=msttfIHHkak

在第一个渲染中,通过在二维数组(包括起始节点和结束节点)上进行映射生成网格。

如果单击并拖动到网格上,则将其切换为开/关,但这会导致整个网格被重新渲染两次,尽管只应重新渲染在此过程中修改的节点。

我无法弄清楚仅当传递下来的道具发生变化时才重新渲染节点。

1 个答案:

答案 0 :(得分:2)

重新渲染的问题是,即使使用useCallback方法,实际上您是在nodeGrid更改时重新创建函数,因此无法利用Node上React.memo的性能优化组件,这是因为您的所有onMouseDown, onMouseEnter, onMouseLeave处理函数都已重新创建

此外,当您将mouseIsPressed用作状态时,由于这种情况,您也被迫触发重新渲染并再次创建回调。

这里的解决方案是利用状态更新回调,还使用mouseIsPressed作为参考而不是状态

const [nodeGrid, setNodeGrid] = useState({
    grid: []
  });

  const mouseIsPressed = useRef(false);

  useEffect(() => {
    const grid1 = getInitialGrid();
    setNodeGrid({ ...nodeGrid, grid: grid1 });
  }, []);

  const handleMouseDown = useCallback((row, col) => {
    //console.log(newGrid);
    setNodeGrid(prevGrid => ({
      grid: getNewGridWithWallToggled(prevGrid.grid, row, col)
    }));
    mouseIsPressed.current = true;
    //console.log(nodeGrid);
  }, []);

  const handleMouseEnter = useCallback((row, col) => {
    //console.log(mouseIsPressed);
    if (mouseIsPressed.current) {
      setNodeGrid(prevNodeGrid => ({
        ...prevNodeGrid,
        grid: getNewGridWithWallToggled(prevNodeGrid.grid, row, col)
      }));
    }
  }, []);

  const handleMouseUp = useCallback(() => {
    mouseIsPressed.current = false;
  }, []);

Optimized DEMO