1 题目背景

首先给一下树型选择器的数据结构

  1. interface ITreeNode {
  2. label: string;
  3. value: string;
  4. desc?: string;
  5. disabled?: boolean;
  6. children?: ITreeNode[];
  7. }

2 需求

我们可以拿到的是selectNow,是一个value数组,当该数组增加某值时,需要完成两件事:

  1. 该节点下的所有子孙节点全部选中
  2. 自动检测是否有某层(当然肯定是最后那个节点那层),若有,则该层父节点选中

    注意:父节点选中后可能再导致有新的可选中节点

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 继续优化

加入了对于类型的判断,是增还是减。

有两种情况得到的数据是一样的:

  1. 新增一个节点
  2. 两个节点取消勾选一个节点

因此有必要加一个类型判断,并基于此进行对应的操作

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,以进行下一轮遍历。
算法还需优化,时间复杂度过高,对于一棵复杂的树递归里面套递归容易裂开,写法也有待优化。