基于react的tree组件的简单实现

本文介绍了如何使用React实现一个Tree组件,包括实现点击节点展开子节点的功能,以及通过关系矩阵和指数追踪优化数据处理。提供在线示例链接,详细解释了如何处理节点数据和优化节点间关系的表示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在线示例https://codesandbox.io/embed/myp00yr1xx?fontsize=14/react-tree2

实现的效果

  • 点击分支节点时,展开其子节点
  • 点击任意节点都能十分容易的确定对应的节点数据
  • 能接受形如下列数组格式的原始节点数据:
   [
       {
         id: 5,
         name: "节点0-3-5",
         pid: 3
       },
       {
         id: 4,
         name: "节点0-3-4",
         pid: 3
       },
       {
         id: 0,
         name: "根节点",
         pid: -1
       },
       {
         id: 6,
         name: "节点0-1-6",
         pid: 1
       },
       {
         id: 2,
         name: "节点0-2",
         pid: 0
       },
       {
         id: 3,
         name: "节点0-3",
         pid: 0
       },
       {
         id: 1,
         name: "节点0-1",
         pid: 0
       }
     ]
  • 能接受形如下列形式的Object的原始节点数据:
	{
        id: 0,
        name: "根节点",
        pid: -1,
        children: [
          {
            id: 1,
            name: "节点0-1",
            pid: 0,
            children: [
              {
                id: 6,
                name: "节点0-1-6",
                pid: 1
              }
            ]
          },
          {
            id: 2,
            name: "节点0-2",
            pid: 0
          },
          {
            id: 3,
            name: "节点0-3",
            pid: 0,
            children: [
              {
                id: 4,
                name: "节点0-3-4",
                pid: 3
              },
              {
                id: 5,
                name: "节点0-3-5",
                pid: 3
              }
            ]
          }
        ]
      }

关系矩阵

在数据结构中,我们分析图结构的节点之间的关系时,经常提到关系矩阵。我们可以用二维数组定义一个这样的关系矩阵。

// 假设原始节点数据是数组
const createShipMatrix = (origin) => {
  if (!origin || !origin.length) return;
  const shipMatrix = [];
  // 初始化
  const len = origin.length;
  for (let i = 0; i<len; i++ ){
    	shipMatrix[i]=[];
   }
  // 假设有两个节点origin[a]与origin[b],我们令
  //   shipMatrix[a][b] = 1,表示orgin[a]是origin[b]的父节点;
  //   shipMatrix[a][b] = -1,表示orgin[a]是origin[b]的子节点;
  //   shipMatrix[a][b] = 0,表示orgin[a]和origin[b]没有直接关系。
  for (let i = 0; i<len; i++) {
      for (let j = 0; j<len; j++) {
         if (i==j){
            shipMatrix[i][j] = 0;
         } else if(origin[i].id === origin[j].pid) {
           shipMatrix[i][j] = 1;
		 } else if (origin[i].pid === origin[j].id) {
			shipMatrix[i][j] = -1;
         } else {
    		shipMatrix[i][j] = 0;
         }
      }
  }
  return shipMatrix;
}	

通过这样一个关系矩阵,我们可以清楚的知道每个节点之间的关系。

index追踪

关系矩阵耗费的资源实在太大了,时间复杂度和空间复杂度都达到了n^2 。下面是改进方案,采用index追踪的方式,为了方便追踪操作的节点为每个节点数据增加了parentIndex和index属性:

  export const isEmpty = data => {
  if (
    data === null ||
    data === undefined ||
    `${data}`.trim() === "" ||
    (data !== 0 && !data)
  ) {
    return true;
  }
  return false;
};

export const treeNode = ({ id, pid, name, ...extra }) => {
  const empty = isEmpty(id) || isEmpty(pid);
  if (empty) {
    console.error(
      { id, pid },
      "id和pid不能为null, undefined,NaN,以及空字符串."
    );
    return;
  }

  return {
    id,
    name,
    pid,
    ...extra
  };
};

/**
 * @param {object[]} arr 原始节点数组
 * @returns {Object} 改造后的节点数组增加chilren、parentIndex
 * 和index属性数组的第一个元素为根节点
 */
export const arrayToTreeNodesArray = arr => {
  if (!Array.isArray(arr)) {
    console.error("参数必须是数组");
    return;
  }

  const nodes = {};

  // 转换成普通Object形式, 记录index
  arr.forEach((item, index) => {
    const nodeItem = treeNode(item);
    if (!nodeItem) return;
    nodes[nodeItem.id] = index;
  });

  const treeNodeArr = [...arr];
  let rootNodeIndex;
  Object.keys(nodes).find(key => {
    const index = nodes[key];
    const currentNode = arr[index];
    const { pid } = currentNode;
    if (`${pid}` === "-1" && (rootNodeIndex || rootNodeIndex === 0)) {
      console.error("不止一个根节点", {
        rootNodeIndexes: [rootNodeIndex, index]
      });
      return true;
    } else if (`${pid}` === "-1") {
      rootNodeIndex = index;
    }
    let parentIndex = nodes[pid];

    treeNodeArr[index] = { ...treeNodeArr[index], parentIndex, index };
    if ((!parentIndex && parentIndex !== 0) || `${parentIndex}` === "-1")
      return false;

    if (!treeNodeArr[parentIndex].children) {
      treeNodeArr[parentIndex] = { ...treeNodeArr[parentIndex], children: [] };
    }
    treeNodeArr[parentIndex].children.push(index);
    return false;
  });

  if (rootNodeIndex || rootNodeIndex === 0) {
    treeNodeArr[rootNodeIndex].hidden = false;
    return { treeNodeArr, rootNodeIndex };
  }
  console.error("没有根节点");
  return;
};

/**
 * @param {object} treeObject 将形如{id, name, pid, children}
 * 的object数据转换为树形结构数组
 */
export const objectToTreeNodesArray = treeObject => {
  const treeNodeArr = [];
  function objectToTreeArr(sourceObject, parentIndex) {
    if (!sourceObject.children || sourceObject.children.length === 0) {
      treeNodeArr.push({
        ...sourceObject,
        parentIndex,
        index: treeNodeArr.length
      });
      return;
    }

    const { children, ...rest } = sourceObject;
    const currentNode = {
      ...rest,
      children: [],
      parentIndex,
      index: treeNodeArr.length
    };
    treeNodeArr.push(currentNode);
    for (let i = 0, len = children.length; i < len; i++) {
      currentNode.children.push(treeNodeArr.length);
      objectToTreeArr(children[i], currentNode.index);
    }
  }
  objectToTreeArr(treeObject);
  return { treeNodeArr, rootNodeIndex: 0 };
};

export const createTreeNodesArray = treeNodes => {
  if (Array.isArray(treeNodes)) {
    return arrayToTreeNodesArray(treeNodes);
  }
  if (typeof treeNodes === "object") {
    return objectToTreeNodesArray(treeNodes);
  }
};

export const changeExpandStatus = (node, treeNodes) => {
  const { children, expanded = false } = node || {};
  if (!children || children.length === 0) return;
  const nodes = [...treeNodes];
  nodes[node.index] = { ...node, expanded: !expanded };
  return nodes;
}

接下来我们可以根据构造的追踪数组来从根节点开始构造树形结构了。

const TreeNodes = ({ render, rootNodeIndex, treeNodes, onNodeClick }) => {
  // console.info("优化Tree---TreeNodes---", {
  //   render,
  //   rootNodeIndex,
  //   treeNodes,
  //   onNodeClick
  // });
  const createNodes = (currentParent, currentLevel) => {
    const node = treeNodes[currentParent];
    // console.info("优化Tree---createNodes---", {
    //   node,
    //   currentParent,
    //   currentLevel
    // });
    const children = node.children;
    return (
      <Node
        key={`node-${node.id}`}
        data={node}
        level={currentLevel}
        onNodeClick={onNodeClick}
        render={render}
        expanded={node.expanded}
      >
        {node.expanded && children && children.length && (
          <ul className="tree-branch-node">
            {children.map(item => createNodes(item, currentLevel + 1))}
          </ul>
        )}
      </Node>
    );
  };
  return createNodes(rootNodeIndex, 0);
};

const Node = React.memo(props => {
  const {
    expanded = false,
    data,
    children,
    level,
    onNodeClick,
    render
  } = props;
  const isLeafNode = !data.children || !data.children.length;
  console.info("优化Tree组件--Node--", props);
  const { name } = data;
  const iconClass = expanded ? "icon-jiantou_xia" : "icon-jiantou_you";
  return (
    <li className={`${isLeafNode ? "tree-leaf-node" : ""} tree-li`}>
      <span
        onClick={e => onNodeClick(e, data)}
        className={` ${
          isLeafNode ? "" : "branch-icon-text-container"
        } level${level}`}
      >
        {!isLeafNode && (
          <span className={`iconfont ${iconClass} branch-icon`} />
        )}
        <span>
          {render && typeof render === "function"
            ? render({ node: data, level })
            : name}
        </span>
      </span>
      {children}
    </li>
  );
});

export default React.memo(props => {
  const { treeNodes, onNodeClick, render } = props;
  if ((Array.isArray(treeNodes) && !treeNodes.length) || !treeNodes) return "";

  const [nodesArrObj, setNodesArrObj] = useState({});
  const { rootNodeIndex, treeNodeArr } = nodesArrObj || {};

  useEffect(() => {
    const nodesObject = createTreeNodesArray(treeNodes);
    setNodesArrObj(nodesObject);
  }, [treeNodes]);

  if (
    (!rootNodeIndex && rootNodeIndex !== 0) ||
    !treeNodeArr ||
    !treeNodeArr.length
  )
    return "";

  const nodeClickHandler = (e, node) => {
    e.stopPropagation();
    onNodeClick(e, node);
    const newNodesArr = changeExpandStatus(node, treeNodeArr);
    if (newNodesArr && newNodesArr.length > 0) {
      setNodesArrObj({ rootNodeIndex, treeNodeArr: newNodesArr });
    }
  };

  return (
    <ul className="tree-container">
      <TreeNodes
        rootNodeIndex={rootNodeIndex}
        treeNodes={treeNodeArr}
        onNodeClick={nodeClickHandler}
        render={render}
      />
    </ul>
  );
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值