在线示例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>
);
});