前端常用的树处理方法总结

实际项目中比较常遇到树相关的处理,所以记录一下。

主要是下面几个功能

  • 数据源转换成指定树结构 (目前数据源都是数组树)
  • 模糊查询树 (根据节点名称)
  • 查找指定key节点
  • 查找指定key节点的父节点路径
  • 查找指定key节点的所有子节点
ts 复制代码
import { TreeDataNode } from "antd";

export type TRecord = {
  [key: string]: any;
};

/**@param 转换后节点类型/
export type TNode = {
  /**@param 数据源节点/
  data: TRecord;
  /**@param 转换后节点的子节点/
  children: TNode[];
  /**@param 是否高亮,用于模糊查询UI渲染/
  highLight: boolean;
} & TreeDataNode;

/**@param 模糊查询树结果/
export interface IFilterTreeResult {
  /**@param 模糊查询后的树/
  filterTree: TNode[];
  /**@param 模糊查询后的树节点key集合/
  filterKeys: string[];
}

/**@param 节点与数据源节点字段映射关系/
export interface ITreeConfig {
  /**@param 节点label对应的字段名 */
  labelKey: string;
  /**@param 节点value对应的字段名 */
  valueKey: string;
  /**@param 节点key对应的字段名 */
  keyKey: string;
  /**@param 节点children对应的字段名 */
  childrenKey: string;
}

export interface ITreeProcessor {
  /**@function 转换指定树结构 */
  processTree(tree: TRecord[], nameKey: string, childrenName: string): TNode[];
  /**@function 模糊查询树,返回过滤后的树和满足条件的节点路径 */
  filterTree(predicate: (node: TNode) => boolean): IFilterTreeResult;
  /**@function 获取节点key与节点本身的映射 */
  getNodeMap(): Map<string, TNode>;
  /**@function 获取节点key与上级父节点key的映射 */
  getParentMap(): Map<string, string | null>;
  /**@function 向外暴露转换后的树 */
  getProcessTree(): TNode[];
  /**@function 清除节点映射 */
  clearNodeMap(): void;
  /**@function 清除过滤匹配映射 */
  clearFilterMatchMap(): void;
}

class TreeProcessor implements ITreeProcessor {
  /**@param 树 */
  private tree: TNode[];
  /**@param */
  private treeConfig: ITreeConfig;
  /**@param 节点key与节点本身的映射 */
  private nodeMap: Map<string, TNode>;
  /**@param 节点key与父节点key的映射 */
  private parentMap: Map<string, string | null>;
  /**@param 匹配过滤条件节点的key集合 */
  private matchedIds: Set<string>;
  /**@param 匹配过滤条件节点的所有子节点key集合 */
  private matchedChildrenIds: Set<string>;

  constructor(config: Partial<ITreeConfig> = {}) {
    this.tree = [];
    this.nodeMap = new Map();
    this.parentMap = new Map();
    this.matchedIds = new Set<string>();
    this.matchedChildrenIds = new Set<string>();
    this.treeConfig = {
      labelKey: config.labelKey || 'label',
      valueKey: config.valueKey || 'value',
      keyKey: config.keyKey || 'id',
      childrenKey: config.childrenKey || 'children',
    }
  }

  processTree(tree: TRecord[]): TNode[] {
    this.clearNodeMap();

    const processNode = (
      nodes: TRecord[],
      parentId: string | null
    ): TNode[] => {
      return nodes.map((node) => {
        const beforeNode = {
          ...node,
        };
        delete beforeNode[this.treeConfig.childrenKey];
        const newNode = {
          key: node[this.treeConfig.keyKey],
          title: node[this.treeConfig.labelKey] ?? "",
          data: beforeNode,
          highLight: false,
          children: [] as TNode[],
        };
        this.nodeMap.set(node[this.treeConfig.keyKey], newNode);
        this.parentMap.set(node[this.treeConfig.keyKey], parentId);

        if (Array.isArray(node[this.treeConfig.childrenKey])) {
          newNode.children = processNode(node[this.treeConfig.childrenKey], node[this.treeConfig.keyKey]);
        }
        return newNode;
      });
    };

    const result = processNode(tree, null);
    this.tree = result;
    return result;
  }

  filterTree(predicate: (node: TNode) => boolean) {
    // 计算路径
    Array.from(this.nodeMap.values())
      .filter(predicate)
      .forEach((node) => {
        this.matchedIds.add(node.key as string);
        this.markAncestors(node.key as string);
        this.markChildrenAncestors(node.key as string);
      });

    const filterTree = this.buildFilterTree(predicate);
    const filterKeys = Array.from(
      new Set([...this.matchedIds, ...this.matchedChildrenIds])
    );

    this.clearFilterMatchMap();
    return {
      filterTree,
      filterKeys,
    };
  }

  getNodeMap() {
    return this.nodeMap;
  }

  getParentMap() {
    return this.parentMap;
  }

  getProcessTree() {
    return this.tree;
  }

  clearNodeMap() {
    this.nodeMap.clear();
    this.parentMap.clear();
  }

  clearFilterMatchMap() {
    this.matchedIds.clear();
    this.matchedChildrenIds.clear();
  }

  /**@function 构建过滤匹配后的树 */
  private buildFilterTree(predicate: (node: TNode) => boolean) {
    const loop = (nodes: TNode[]): TNode[] => {
      return nodes
        .filter(
          (node) =>
            this.matchedIds.has(node.key as string) ||
            this.matchedChildrenIds.has(node.key as string)
        )
        .map((node) => ({
          ...node,
          highLight: predicate(node),
          children: node.children ? loop(node.children as TNode[]) : [],
        }));
    };

    return loop(this.tree);
  }

  /**@function 获取指定key节点的所有父节点key */
  private markAncestors(nodeId: string) {
    let parentId = this.parentMap.get(nodeId);
    while (parentId) {
      this.matchedIds.add(parentId);
      parentId = this.parentMap.get(parentId);
    }
  }

  /**@function 获取指定key节点的所有子节点key */
  private markChildrenAncestors(nodeId: string) {
    const node = this.nodeMap.get(nodeId);
    if (!node) return;
    this.matchedChildrenIds.add(nodeId);
    node.children.forEach((child) =>
      this.markChildrenAncestors(child.key as string)
    );
  }
}

export default TreeProcessor;

思路是:

  • 转换树结构时,根据 treeConfig 的配置信息,构造指定的树节点,并记录相关的map信息
  • 模糊查询时,过滤出匹配的节点,然后通过map去查找匹配节点的父节点key路径。然后这里因为项目需要,也去找了子节点key路径。然后再根据把匹配的节点高亮
相关推荐
phltxy15 分钟前
Vue 核心特性实战指南:指令、样式绑定、计算属性与侦听器
前端·javascript·vue.js
咖丨喱1 小时前
IP校验和算法解析与实现
网络·tcp/ip·算法
Byron07071 小时前
Vue 中使用 Tiptap 富文本编辑器的完整指南
前端·javascript·vue.js
罗湖老棍子1 小时前
括号配对(信息学奥赛一本通- P1572)
算法·动态规划·区间dp·字符串匹配·区间动态规划
css趣多多1 小时前
地图快速上手
前端
zhengfei6112 小时前
面向攻击性安全专业人员的一体化浏览器扩展程序[特殊字符]
前端·chrome·safari
fengfuyao9852 小时前
基于MATLAB的表面织构油润滑轴承故障频率提取(改进VMD算法)
人工智能·算法·matlab
机器学习之心2 小时前
基于随机森林模型的轴承剩余寿命预测MATLAB实现!
算法·随机森林·matlab
一只小小的芙厨2 小时前
寒假集训笔记·树上背包
c++·笔记·算法·动态规划
马猴烧酒.2 小时前
【面试八股|Java集合】Java集合常考面试题详解
java·开发语言·python·面试·八股