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

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

主要是下面几个功能

  • 数据源转换成指定树结构 (目前数据源都是数组树)
  • 模糊查询树 (根据节点名称)
  • 查找指定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路径。然后再根据把匹配的节点高亮
相关推荐
前端工作日常2 小时前
我理解的`npm pack` 和 `npm install <local-path>`
前端
_一条咸鱼_2 小时前
Android Runtime堆内存架构设计(47)
android·面试·android jetpack
李剑一2 小时前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华2 小时前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言2 小时前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端
奇舞精选3 小时前
Prompt 工程实用技巧:掌握高效 AI 交互核心
前端·openai
Danny_FD3 小时前
React中可有可无的优化-对象类型的使用
前端·javascript
用户757582318553 小时前
混合应用开发:企业降本增效之道——面向2025年移动应用开发趋势的实践路径
前端
P1erce3 小时前
记一次微信小程序分包经历
前端
LeeAt3 小时前
从Promise到async/await的逻辑演进
前端·javascript