Skip to content

@utilslib/core/flattenTreeArray


打平嵌套的树形结构,并为每个节点添加 level 和 parentId 字段。

支持 Array、Map、Set、Object 等多种集合类型

flattenTreeArray

函数签名

typescript
function flattenTreeArray<T extends {
    level?: never;
    parentId?: never;
    [key: string]: any;
  }, P extends keyof T, ID extends keyof T, R = T & { level: number; parentId: T[ID] }>(collection: T[] | Map<any, T> | Set<T> | Record<string, T>, childrenProperty: P, idAttr: ID, includeParent: boolean = true): R[]

描述

打平嵌套的树形结构,并为每个节点添加 level 和 parentId 字段。 支持 Array、Map、Set、Object 等多种集合类型

类型参数

参数名约束默认值描述
T\{ level?: never; parentId?: never; \[key: string\]: any; \}-- 树节点类型
Pkeyof T-- 子节点属性的键名类型
IDkeyof T-- 节点 ID 属性的键名类型
R-T & \{ level: number; parentId: T\[ID\] \}- 返回类型

参数

参数名类型可选默认值描述
collectionT[] | Map<any, T> | Set<T> | Record<string, T>--
childrenPropertyP--
idAttrID--
includeParentbooleantrue-

返回值

R\[\]

点击查看源码
js
/**
 * 检查集合是否非空
 */
function isNonEmptyCollection(data) {
  if (!data) return false;
  if (Array.isArray(data)) return data.length > 0;
  if (data instanceof Map || data instanceof Set) return data.size > 0;
  if (typeof data === "object") return Object.keys(data).length > 0;
  return false;
}
/**
 * 通用的条目迭代器
 * - Map: [key, value]
 * - Set: [value, value]
 * - Array/Iterable: [index, value]
 * - Object: [key, value]
 */
function* entries(data) {
  if (data instanceof Map) yield* data;
  else if (data instanceof Set) {
    for (const v of data) yield [v, v];
  } else if (typeof data[Symbol.iterator] === "function") {
    let i = 0;
    for (const v of data) yield [i++, v];
  } else if (data) {
    yield* Object.entries(data);
  }
}
/**
 * 使用深度优先搜索(DFS)遍历树形结构,并为每个节点调用回调函数
 * 支持遍历 Array、Map、Set、Object 等多种集合类型
 *
 * @template T - 树节点类型
 * @param {T | T[] | Map | Set | Object | null | undefined} o - 要遍历的树或节点集合
 * @param {keyof T} childrenKey - 子节点属性的键名
 * @param {WalkTreeCallback<T>} cb - 遍历时的回调函数
 */
function walkTree(o, childrenKey, cb) {
  if (!o) return;
  // 保持根节点的原始集合类型
  const rootNodes = Array.isArray(o)
    ? o
    : o instanceof Map || o instanceof Set || typeof o === "object"
      ? o
      : [o];
  let breakLoop = false;
  // DFS递归遍历函数
  function dfsTraverse(nodes, level, ancestors = []) {
    if (!isNonEmptyCollection(nodes)) return false;
    if (breakLoop) return true;
    // 使用 entries 遍历,支持多种集合类型
    for (const [index, node] of entries(nodes)) {
      const typedNode = node;
      const children = typedNode[childrenKey];
      const leaf = !isNonEmptyCollection(children);
      // 调用回调函数
      cb(
        typedNode,
        {
          level,
          index: index,
          leaf,
          peerList: nodes,
          parent: ancestors.slice(-1)[0]?.data,
          ancestors: [...ancestors],
        },
        () => {
          breakLoop = true;
        },
      );
      if (breakLoop) return true;
      if (!leaf) {
        const newAncestors = [...ancestors, { index: index, data: typedNode }];
        if (dfsTraverse(children, level + 1, newAncestors)) return true;
      }
    }
    return false;
  }
  dfsTraverse(rootNodes, 0);
}
/**
 * 打平嵌套的树形结构,并为每个节点添加 level 和 parentId 字段。
 * 支持 Array、Map、Set、Object 等多种集合类型
 *
 * @template T - 树节点类型
 * @template P - 子节点属性的键名类型
 * @template ID - 节点 ID 属性的键名类型
 * @template R - 返回类型
 * @param {T[] | Map | Set | Object} collection - 嵌套的树形结构集合
 * @param {P} childrenProperty - 子节点属性的键名
 * @param {ID} idAttr - 节点 ID 属性的键名
 * @param {boolean} [includeParent=true] - 是否包含父节点,默认为 true
 * @returns {R[]} 打平后的数组
 */
export function flattenTreeArray(
  collection,
  childrenProperty,
  idAttr,
  includeParent = true,
) {
  const result = [];
  walkTree(collection, childrenProperty, (node, context) => {
    const { level, leaf, parent } = context;
    const data = node;
    if (leaf || includeParent) {
      result.push(
        Object.assign({}, data, { level, parentId: parent?.[idAttr] }),
      );
    }
  });
  return result;
}
ts
/**
 * 遍历树的回调函数类型
 * @template T - 树节点类型
 * @template C - 子节点集合类型
 */
type WalkTreeCallback<T, C = any> = (
  item: T,
  context: {
    /** 当前节点的层级 */
    level: number;
    /** 当前节点在同级节点中的索引(数组为number,Map/Object为key,Set为值本身) */
    index: number | string | T;
    /** 当前节点是否为叶子节点 */
    leaf: boolean;
    /** 当前节点的同级节点列表(保持原始集合类型) */
    peerList: C;
    /** 当前节点的父节点信息 */
    parent?: T;
    /** 当前节点的所有祖先节点列表 */
    ancestors: ParentInfo<T>[];
  },
  // 停止遍历
  breakLoop: () => void,
) => void;

/**
 * 父节点信息接口
 * @template T - 树节点类型
 */
export interface ParentInfo<T> {
  /** 在同级节点中的索引(数组为number,Map/Object为key,Set为值本身) */
  index: number | string | T;
  /** 父节点数据 */
  data: T;
}

/**
 * 检查集合是否非空
 */
function isNonEmptyCollection(data: any): boolean {
  if (!data) return false;
  if (Array.isArray(data)) return data.length > 0;
  if (data instanceof Map || data instanceof Set) return data.size > 0;
  if (typeof data === "object") return Object.keys(data).length > 0;
  return false;
}

/**
 * 通用的条目迭代器
 * - Map: [key, value]
 * - Set: [value, value]
 * - Array/Iterable: [index, value]
 * - Object: [key, value]
 */
function* entries<T, K, V>(
  data: Map<K, V> | Set<T> | Iterable<T> | Record<string, any>,
): Generator<[K | number | string | T, V | T], void, unknown> {
  if (data instanceof Map) yield* data;
  else if (data instanceof Set) {
    for (const v of data) yield [v, v] as [T, T];
  } else if (typeof (data as Iterable<T>)[Symbol.iterator] === "function") {
    let i = 0;
    for (const v of data as Iterable<T>) yield [i++, v] as [number, T];
  } else if (data) {
    yield* Object.entries(data) as [string, any][];
  }
}

/**
 * 使用深度优先搜索(DFS)遍历树形结构,并为每个节点调用回调函数
 * 支持遍历 Array、Map、Set、Object 等多种集合类型
 *
 * @template T - 树节点类型
 * @param {T | T[] | Map | Set | Object | null | undefined} o - 要遍历的树或节点集合
 * @param {keyof T} childrenKey - 子节点属性的键名
 * @param {WalkTreeCallback<T>} cb - 遍历时的回调函数
 */
function walkTree<T extends Record<string, any>, K extends keyof T>(
  o: T | T[] | Map<any, T> | Set<T> | Record<string, T> | null | undefined,
  childrenKey: K,
  cb: WalkTreeCallback<T>,
): void {
  if (!o) return;

  // 保持根节点的原始集合类型
  const rootNodes = Array.isArray(o)
    ? o
    : o instanceof Map || o instanceof Set || typeof o === "object"
      ? o
      : [o];
  let breakLoop = false;

  // DFS递归遍历函数
  function dfsTraverse(
    nodes: any,
    level: number,
    ancestors: ParentInfo<T>[] = [],
  ): boolean {
    if (!isNonEmptyCollection(nodes)) return false;
    if (breakLoop) return true;

    // 使用 entries 遍历,支持多种集合类型
    for (const [index, node] of entries(nodes)) {
      const typedNode = node as T;
      const children = typedNode[childrenKey];
      const leaf = !isNonEmptyCollection(children);

      // 调用回调函数
      cb(
        typedNode,
        {
          level,
          index: index as number | string | T,
          leaf,
          peerList: nodes,
          parent: ancestors.slice(-1)[0]?.data,
          ancestors: [...ancestors],
        },
        () => {
          breakLoop = true;
        },
      );

      if (breakLoop) return true;

      if (!leaf) {
        const newAncestors = [
          ...ancestors,
          { index: index as number | string | T, data: typedNode },
        ];
        if (dfsTraverse(children, level + 1, newAncestors)) return true;
      }
    }

    return false;
  }

  dfsTraverse(rootNodes, 0);
}

/**
 * 打平嵌套的树形结构,并为每个节点添加 level 和 parentId 字段。
 * 支持 Array、Map、Set、Object 等多种集合类型
 *
 * @template T - 树节点类型
 * @template P - 子节点属性的键名类型
 * @template ID - 节点 ID 属性的键名类型
 * @template R - 返回类型
 * @param {T[] | Map | Set | Object} collection - 嵌套的树形结构集合
 * @param {P} childrenProperty - 子节点属性的键名
 * @param {ID} idAttr - 节点 ID 属性的键名
 * @param {boolean} [includeParent=true] - 是否包含父节点,默认为 true
 * @returns {R[]} 打平后的数组
 */
export function flattenTreeArray<
  T extends {
    level?: never;
    parentId?: never;
    [key: string]: any;
  },
  P extends keyof T,
  ID extends keyof T,
  R = T & { level: number; parentId: T[ID] },
>(
  collection: T[] | Map<any, T> | Set<T> | Record<string, T>,
  childrenProperty: P,
  idAttr: ID,
  includeParent: boolean = true,
): R[] {
  const result: R[] = [];
  walkTree(collection, childrenProperty, (node, context) => {
    const { level, leaf, parent } = context;
    const data = node;

    if (leaf || includeParent) {
      result.push(
        Object.assign({}, data, { level, parentId: parent?.[idAttr] }) as R,
      );
    }
  });

  return result;
}

如有错误,请提交issue :::