1 题目背景
首先给一下树型选择器的数据结构
interface ITreeNode {label: string;value: string;desc?: string;disabled?: boolean;children?: ITreeNode[];}
2 需求
我们可以拿到的是selectNow,是一个value数组,当该数组增加某值时,需要完成两件事:
- 该节点下的所有子孙节点全部选中
- 自动检测是否有某层(当然肯定是最后那个节点那层),若有,则该层父节点选中
注意:父节点选中后可能再导致有新的可选中节点
3 基本完成
下面是我花1个小时堆出来的💩山
const selectTreeNode = (treeData: ITreeNode[], selectNow: string[]) => {
/** 第一轮,往下走,子孙节点全选 */
// 扫描找到对应节点函数
const scanNode = (
waitingScanStack: ITreeNode[],
fatherValue: string
): ITreeNode[] => {
let newWaitingScanStack: ITreeNode[] = [];
waitingScanStack.map((node: ITreeNode) => {
if (node.value === fatherValue) {
haveFound = true;
if (node.children) {
targetChildren = node.children;
}
} else if (node.value !== fatherValue && node.children) {
newWaitingScanStack.push(...node.children);
}
});
return newWaitingScanStack;
};
// 添加子节点函数
const selectChildren = (targetChildren: ITreeNode[]) => {
let newTargetChildren: ITreeNode[] = [];
targetChildren.map((child) => {
// 自己如果不在seletNow,把自己加进去
if (selectNow.indexOf(child.value) === -1) {
selectNow.push(child.value);
}
// 如果有孩子,把孩子继续返回
if (child.children) {
newTargetChildren.push(...child.children);
}
});
return newTargetChildren;
};
// 假设只考虑最后一个节点为新加
let lastNodeValue = selectNow[selectNow.length - 1];
let waitingScanStack: ITreeNode[] = treeData;
let targetChildren: ITreeNode[] = [];
let haveFound: boolean = false;
// 以找到需要的子节点数组为退出循环标志,这里获取到targetChildren
while (!haveFound) {
waitingScanStack = scanNode(waitingScanStack, lastNodeValue);
}
// 只要targetChildren不为空,就循环点亮下一批
while (targetChildren.length !== 0) {
targetChildren = selectChildren(targetChildren);
}
/** 第二轮,往上走,若子节点全满,则父节点亮(且需继续判断 */
let isChange: boolean = true;
while (isChange) {
isChange = false;
treeData.map((node) => {
// 有孩子且自己未点亮才继续执行
if (node.children && selectNow.indexOf(node.value) === -1) {
let childrenAllSelected = true;
node.children.map((childNode) => {
if (selectNow.indexOf(childNode.value) === -1) {
childrenAllSelected = false;
}
});
if (childrenAllSelected) {
isChange = true;
selectNow.push(node.value);
}
}
});
}
console.log(selectNow);
return selectNow;
};
4 继续优化
加入了对于类型的判断,是增还是减。
有两种情况得到的数据是一样的:
- 新增一个节点
- 两个节点取消勾选一个节点
因此有必要加一个类型判断,并基于此进行对应的操作
const selectTreeNode = (
treeData: ITreeNode[],
selectNow: string[],
selectOld: string[]
) => {
/**
* 初始处理
*/
const getDiffValue = (arr1: string[], arr2: string[]): string => {
let [longArr, shortArr] =
arr1.length < arr2.length
? [[...arr2], [...arr1]]
: [[...arr1], [...arr2]];
while (longArr) {
let cur = longArr.shift();
if (shortArr.indexOf(cur!) === -1) {
return cur!;
} else {
shortArr.splice(shortArr.indexOf(cur!), 1);
}
}
return '';
};
const handleType: number = Number(selectNow.length < selectOld.length); // 0-新增节点 1-删除节点
const changeValue: string = getDiffValue(selectNow, selectOld); // 改变节点的value,等待封装diff
let waitingScanStack: ITreeNode[] = treeData; // 扫描找节点暂存栈
let haveFound: boolean = false; // 是否找到修改节点
let targetChildren: ITreeNode[] = []; // 子节点操作暂存栈
let isChange: boolean = true; // 操作父节点暂存状态
/**
* 第一轮,往下走,子孙节点全选
*/
// 扫描找到对应节点函数
const scanNode = (
waitingScanStack: ITreeNode[],
fatherValue: string
): ITreeNode[] => {
let newWaitingScanStack: ITreeNode[] = [];
waitingScanStack.map((node: ITreeNode) => {
if (node.value === fatherValue) {
haveFound = true;
if (node.children) {
targetChildren = node.children;
}
} else if (node.value !== fatherValue && node.children) {
newWaitingScanStack.push(...node.children);
}
});
return newWaitingScanStack;
};
// 操作子节点函数
const handleTargetChildren = (targetChildren: ITreeNode[]) => {
let newTargetChildren: ITreeNode[] = [];
targetChildren.map((child) => {
// 如果在now数组中,去掉
if (selectNow.indexOf(child.value) !== -1 && handleType) {
selectNow.splice(selectNow.indexOf(child.value), 1);
} else if (selectNow.indexOf(child.value) === -1 && !handleType) {
selectNow.push(child.value);
}
// 如果有孩子,把孩子继续返回
if (child.children) {
newTargetChildren.push(...child.children);
}
});
return newTargetChildren;
};
// 以找到需要的子节点数组为退出循环标志,这里获取到targetChildren
while (!haveFound) {
waitingScanStack = scanNode(waitingScanStack, changeValue);
}
// 处理targetChildren直到完成
while (targetChildren.length !== 0) {
targetChildren = handleTargetChildren(targetChildren);
}
/**
* 第二轮,往上走,若子节点全满,则父节点亮(且需继续判断
*/
while (isChange) {
isChange = false;
treeData.map((node) => {
// 新增:有孩子且自己未点亮时才继续执行
if (
node.children &&
selectNow.indexOf(node.value) === -1 &&
!handleType
) {
let childrenAllSelected = true;
node.children.map((childNode) => {
if (selectNow.indexOf(childNode.value) === -1) {
childrenAllSelected = false;
}
});
if (childrenAllSelected) {
isChange = true;
selectNow.push(node.value);
}
} else if (
node.children &&
selectNow.indexOf(node.value) !== -1 &&
handleType
) {
// 删除:有孩子且自己勾选时才继续执行
let childrenAllSelected = true;
node.children.map((childNode) => {
if (selectNow.indexOf(childNode.value) === -1) {
childrenAllSelected = false;
}
});
if (!childrenAllSelected) {
isChange = true;
selectNow.splice(selectNow.indexOf(node.value), 1);
}
}
});
}
console.log(selectNow);
return selectNow;
};
5 究极优化
很显然这一部分不是我写的,但是记录一下方便学习。这里主要是封装了两个组件,一个是找到该节点以上的一串父节点,通过数组的方式传递出来;另一个是找到所有的子节点,全都放在一个数组里面,统一操作。
export const findFatherNode = (
newValue: string,
options: ITreeNode[],
result: string[] = []
): string[] => {
if (!options) return [];
for (const option of options) {
result.push(option.value);
if (option.value === newValue) {
result.pop();
return result;
}
if (option.children) {
const findChildren = findFatherNode(newValue, option.children, result);
if (findChildren.length) return findChildren;
}
result.pop();
}
return [];
};
export const getTreeMap = (options: ITreeNode[], treeMap = new Map()) => {
options.forEach((option) => {
treeMap.set(option.value, [option.value]);
if (option.children) {
treeMap.set(option.value, [option.value, ...getTreeMap(option.children)]);
}
});
return treeMap;
};
第二个获取map的思路来自一个资深前端工程师,通过递归,获取到所有的子节点,并用map的形式进行保存,最后得到的是每个节点的所有节点。
6 总结
我自己的实现大致思路就是分成两轮完成。
第一轮处理子孙节点,这部分又分成两部分,首先要通过递归遍历找到该节点在哪里,并且存储下他的子孙节点(若有)。第二部分就是处理之前存储下的子孙节点,也是递归遍历将所有未选中的节点选中。
第二轮处理父节点,通过变量isChange来控制是否有改变,有改变则需要再判断下一轮是否有可选中,若未改变则直接结束。具体操作为递归遍历所有节点,对于自己未选中且有孩子的节点,判断childrenAllSelected?若全选中,则选中该父节点并将isChange改为true,以进行下一轮遍历。
算法还需优化,时间复杂度过高,对于一棵复杂的树递归里面套递归容易裂开,写法也有待优化。
