Skip to content

树形数据相互转换

把树形数据转换成扁平数组

查看代码
ts
// 节点类型 Node Type
interface Node {
  [key: string]: any;
  children?: Array<Node>;
}

// 入参类型
interface TreeToArrayOptions {
  // 子节点的键名称,默认为'children'
  childrenKey?: string;
  // 需要忽略的字段名列表,默认为空列表
  ignoreFields?: Array<string>;
  // 需要添加的字段名及其对应属性值的计算方式列表,默认为空列表
  addFields?: Array<{ fieldName: string; callback: (item: Node) => any }>;
  // 子节点是否需要父节点的id,默认为true
  needParentId?: boolean;
}

// 根据树状结构以数组形式返回所有节点(包括子孙节点)
export const treeToArray = (
  tree: Array<Node>,
  options: TreeToArrayOptions = {}
): Array<Node> => {
  const {
    // 子节点的键名称
    childrenKey = 'children',
    // 要忽略的字段名列表
    ignoreFields = [],
    // 需要添加的字段名及其对应属性值的计算方式列表
    addFields = [],
    // 子节点是否需要父节点的id,默认为true
    needParentId = true
  } = options;
  const nodes: Array<Node> = [];
  // stack用于迭代树结构中的子节点和子孙子节点
  const stack: Array<{
    // 正在处理的节点,包括该节点的信息、父节点的id、以及该节点的所有子节点
    node: Node | null;
    children: Array<Node>;
    parentId: string | null;
  }> = [];
  // 将整个树的所有节点压入栈,其中root节点的parentId为空
  stack.push({
    node: null,
    children: tree,
    parentId: null
  });
  while (stack.length) {
    const { node, children, parentId } = stack.pop()!;
    if (node) {
      // 存储该节点的所有属性到newNode中,除去childrenKey所指定的子节点信息
      const { [childrenKey]: subChildren, ...rest } = node;
      const newNode = { ...rest };
      if (needParentId) {
        // 如果needParentId为true,则将父节点的id添加到该节点的属性中
        newNode['parentId'] = parentId;
      }
      if (addFields.length) {
        // 如果需要添加属性值,则遍历addFields列表,计算对应的属性值
        for (let i = 0; i < addFields.length; i++) {
          newNode[addFields[i].fieldName] = addFields[i].callback(node);
        }
      }
      if (ignoreFields.length) {
        // 如果需要忽略某些属性,则遍历ignoreFields列表,将对应的属性从newNode中删除
        for (let i = 0; i < ignoreFields.length; i++) {
          delete newNode[ignoreFields[i]];
        }
      }
      // 将newNode存入nodes数组中
      nodes.push(newNode);
    }
    if (children) {
      // 将该节点的所有子节点压入栈中,继续循环直到stack为空
      for (let i = children.length - 1; i >= 0; i--) {
        stack.push({
          node: children[i],
          children: children[i][childrenKey] || [],
          parentId: node?.id || ''
        });
      }
    }
  }
  // 返回以数组形式储存的所有节点(包括子孙节点)
  return nodes;
};

参数

接受两个参数:

  • Tree: 树形结构数组

  • Options: 一个可选的参数对象,用于配置转换方法的具体行为

    属性描述类型默认值
    addFields需要添加的字段名称及其对应的属性值计算方法的列表[{ fieldName: string;callback: (item) => any }][]
    childrenKey子节点的键名string'children'
    ignoreFields要忽略的字段名称列表string[][]
    needParentId是否添加节点信息的父节点 IDbooleantrue

示例

javascript
const treeArray = [
  {
    id: '1',
    name: 'Node 1',
    list: [
      {
        id: '2',
        name: 'Node 2',
        list: [
          {
            id: '3',
            name: 'Node 3'
          }
        ]
      },
      {
        id: '4',
        name: 'Node 4'
      }
    ]
  }
]
const calculateDepth = (node) => {
  let depth = 0
  let parent = node
  while (parent) {
    depth++
    parent = parent['parentId'] && treeArray.find((n) => n.id === parent['parentId'])
  }
  return depth
}
const options = {
  childrenKey: 'list',
  ignoreFields: [],
  addFields: [
    {
      fieldName: 'hasChildren', // 添加新字段 'hasChildren',用于判断是否有子节点
      callback: (node) => Boolean(node['children'])
    },
    {
      fieldName: 'depth', // 添加新字段 'depth',用于记录节点深度
      callback: calculateDepth
    }
  ],
  needParentId: true
}

const flatArray = treeToArray(treeArray, options)

console.log(flatArray)

结果如下:

json
[
  { "id": "1", "name": "Node 1", "parentId": "", "hasChildren": false, "depth": 1 },
  { "id": "2", "name": "Node 2", "parentId": "1", "hasChildren": false, "depth": 1 },
  { "id": "3", "name": "Node 3", "parentId": "2", "hasChildren": false, "depth": 1 },
  { "id": "4", "name": "Node 4", "parentId": "1", "hasChildren": false, "depth": 1 }
]

把扁平数组转换成树形数据

查看代码
ts
interface Node {
  id: string; // 节点的唯一标识符
  children?: Array<Node>; // 节点的子节点数组
  pid?: string; // 节点的父节点标识符
}
interface Options {
  idKey?: string; // 自定义 id 字段的名称
  pidKey?: string; // 自定义 pid 字段的名称
  childrenKey?: string; // 自定义 children 字段的名称
}

export const arrayToTree = (
  array: Array<Node | undefined>,
  options: Options = {}
) => {
  if (!Array.isArray(array)) {
    throw new Error('The first argument must be an array.');
  }
  // 解构 options 对象来获取自定义字段名称或默认值
  const { idKey = 'id', pidKey = 'pid', childrenKey = 'children' } = options;
  // 创建节点 id 到节点的映射
  const map = array.reduce((acc: Record<string, Node>, node: any) => {
    acc[node[idKey]] = { ...node, [childrenKey]: [] }; // 初始化节点并添加到映射中
    return acc;
  }, {});

  Object.values(map).forEach((node: any) => {
    // 遍历所有节点
    const parentId = node[pidKey];
    if (parentId) {
      // 如果存在父节点
      const parent: any = map[parentId]; // 获取当前节点的父节点
      if (!parent[childrenKey]) {
        // 如果父节点没有 children 属性
        parent[childrenKey] = []; // 初始化 children 属性
      }
      parent[childrenKey].push(node); // 将当前节点添加到父节点的 children 数组中
    }
  });

  // 找到所有根节点(没有父节点的节点)并构建树
  const tree = Object.values(map).filter((node: any) => !node[pidKey]);

  return tree;
};

参数

它接受两个参数:

  • Array: 扁平的节点数组

  • Options: 一个可选的参数对象,用于配置转换方法的具体行为

    参数描述类型默认值
    childrenKey自定义节点 children 字段名称string'children'
    idKey自定义节点 ID 字段名称string'id'
    pidKey自定义节点父 ID 字段名称string'pid'

示例

javascript
const flatArray = [
  { uid: '1', name: 'node1', pid: null },
  { uid: '2', name: 'node2', pid: '1' },
  { uid: '3', name: 'node3', pid: '1' },
  { uid: '4', name: 'node4', pid: '2' },
  { uid: '5', name: 'node5', pid: '2' },
  { uid: '6', name: 'node6', pid: '3' }
]

const options = {
  idKey: 'id',
  pidKey: 'pid',
  childrenKey: 'children'
}

const treeArray = arrayToTree(flatArray, options)

生成随机树形数据

ts
interface Item {
  // Add any existing properties
  children?: Item[];
  // Define the new property
  id: string;
}
export const generateTree = (depth: number, width: number): Item => {
  const node: Item = {
    id: Math.floor(Math.random() * 1000000).toString()
  };
  if (depth > 0) {
    node.children = [];
    for (let i = 0; i < width; i++) {
      node.children.push(generateTree(depth - 1, width));
    }
  }
  return node;
};

如有转载或 CV 的请标注本站原文地址