有了这篇文章,妈妈再也不担心我不会处理树形结构了!

1,495 阅读11分钟

本篇文章你将学习到。

  1. 什么是树形结构
  2. 一维树形结构 与 多维树形结构 的相互转化。
  3. findTreeDatafilterTreeDatamapTreeData 等函数方法 帮助我们更简单的处理多维树形结构

基础介绍

有很多小白开发可能不知道什么树形结构。这里先简单介绍一下。直接上代码一看就懂

一维树形结构

[
    { id: 1, name: `Node 1`, pId: 0 },
    { id: 2, name: `Node 1.1`, pId: 1 },
    { id: 4, name: `Node 1.1.1`, pId: 2 },
    { id: 5, name: `Node 1.1.2`, pId: 2 },
    { id: 3, name: `Node 1.2`, pId: 1 },
    { id: 6, name: `Node 1.2.1`, pId: 3 },
    { id: 7, name: `Node 1.2.2`, pId: 3 },
    { id: 8, name: `Node 2`, pId: 0 },
    { id: 9, name: `Node 2.1`, pId: 8 },
    { id: 10, name: `Node 2.2`, pId: 8 },
]

多维树形结构

[
    {
      id: 1,
      name: `Node 1`,
      children: [
        {
          id: 2,
          name: `Node 1.1`,
          children: [
            { id: 4, name: `Node 1.1.1`, children: [] },
            { id: 5, name: `Node 1.1.2`, children: [] },
          ],
        },
        {
          id: 3,
          name: `Node 1.2`,
          children: [
            { id: 6, name: `Node 1.2.1`, children: [] },
            { id: 7, name: `Node 1.2.2`, children: [] },
          ],
        },
      ],
    },
    {
      id: 8,
      name: `Node 2`,
      children: [
        { id: 9, name: `Node 2.1`, children: [] },
        { id: 10, name: `Node 2.2`, children: [] },
      ],
    },
 ]

咋一看一维树形结构可能会有点蒙,但是看一下多维树形结构想必各位小伙伴就一目了然了吧。这时候再回头去看一维树形结构想必就很清晰了。一维树形结构就是用pId做关联 来将多维树形结构给平铺了开来。

多维树形结构也是我们前端在渲染页面时经常用到的一种数据结构。但是后台一般给我们的是一维树形结构,而且一维树形结构 也非常有助于我们对数据进行增删改查。所以我们就要掌握一维树形结构多维树形结构的相互转化。

前置规划

再我们进入一段功能开发之前,我们肯定是要设计规划一下,我们的功能架构。

配置项的添加

动态参数名

让我们看一下上面那个数组 很明显有三个属性 是至关重要的。id pIdchildren。可以说没有这些属性就不是树形结构了。但是后台给你的树形结构相关参数不叫这个名字怎么办?所以我们后续的函数方法就要添加一些配置项来动态的配置的属性名。例如这样

type TreeDataConfig = {
  /** 唯一标识 默认是id */
  key?: string
  /** 与父节点关联的唯一标识 默认是pId */
  parentKey?: string
  /** 查询子集的属性名 默认是children */
  childrenName?: string
  isTileArray?: boolean
  isSetPrivateKey?: boolean
}
const flattenTreeData = (treeData:any,config?: TreeDataConfig): T[] => {
    //Do something...
}

keyparentKeychildrenName解决了我们上述的问题。想必你也发现了 除了这些 还有一些其他的配置项。

其他配置项

isTileArray:这个是设置后续的一些操作方法返回值是否为一维树形结构

isSetPrivateKey:这个就是我们下面要说的内容了,是否在节点中添加私有属性。

私有属性的添加

这里先插播一条小知识。可能有的小伙伴会在别人的代码中看到这样一种命名方式 _变量名下划线加变量名,这样就代表了这是一个私有变量。那什么是私有变量呢?请看代码

const name = '张三'
const fun = (_name) =>{
    console.log(_name)
}
fun(name)

上述代码中函数的参数名我们就把他用_name 用来表示。_name就表示了 这个name属性是fun函数的私有变量。用于与外侧的name进行区分。下面我们要添加的私有属性亦是同理 用于与treeNode节点的其他属性进行区分

请继续观察上面的两个树形结构数组。我们会发现多维树形结构的节点中并没有pId属性。这对我们的一些业务场景来说是很麻烦的。因此我们就内置了一个函数 来专门添加这些有可能非常有用的属性。 来更好的描述 我们当前节点在这个树形结构中的位置。

/**
 * 添加私有属性。
 * _pId     父级id
 * _pathArr 记录了从一级到当前节点的id集合。
 * _pathArr 的length可以记录当前是多少层
 * @param treeNode
 * @param parentTreeNode
 * @param treeDataConfig
 */
const setPrivateKey = (treeNode,parentTreeNode, config) => {
  const { key = `id` } = config || {}
  item._pId = parentInfo?.[key]
  item._pathArr = parentInfo?._pathArr ? [...parentInfo._pathArr, item[key]] : [item[key]]
}

一维树形结构 与 多维树形结构 的相互转化

一维树形结构转多维树形结构

/**
 * 一维树形结构转多维树形结构
 * @param tileArray 一维树形结构数组
 * @param config 配置项(key,childrenName,parentKey,isSetPrivateKey)
 * @returns 返回多维树形结构数组
 */
const getTreeData = (tileArray = [], config) => {
  const {
    key = `id`,
    childrenName = `children`,
    parentKey = `pId`,
    isSetPrivateKey = false,
  } = config || {}
  const fun = (parentTreeNode) => {
    const parentId = parentTreeNode[key]
    const childrenNodeList = []
    copyTileArray = copyTileArray.filter(item => {
      if (item[parentKey] === parentId) {
        childrenNodeList.push({ ...item })
        return false
      }
      else {
        return true
      }
    })
    parentTreeNode[childrenName] = childrenNodeList
    childrenNodeList.forEach(item => {
      isSetPrivateKey && setPrivateKey(item, parentTreeNode, config)
      fun(item)
    })
  }
  const rootNodeList = tileArray.filter(item => !tileArray.some(i => i[key] === item[parentKey]))
  const resultArr = []
  let copyTileArray = [...tileArray]
  rootNodeList.forEach(item => {
    const index = copyTileArray.findIndex(i => i[key] === item[key])
    if (index > -1) {
      copyTileArray.splice(index, 1)
      const obj = { ...item }
      resultArr.push(obj)
      isSetPrivateKey && setPrivateKey(obj, undefined, config)
      fun(obj)
    }
  })
  return resultArr
};

多维树形结构转一维树形结构

/**
 * 多维树形结构转一维树形结构
 * @param treeData 树形结构数组
 * @param config 配置项(key,childrenName,isSetPrivateKey)
 * @returns 返回一维树形结构数组
 */
const flattenTreeData = (treeData = [], config) => {
  const { childrenName = `children`, isSetPrivateKey = false } = config || {};
  const result = [];
​
  /**
   * 递归地遍历树形结构,并将每个节点推入结果数组中
   * @param _treeData 树形结构数组
   * @param parentTreeNode 当前树节点的父节点
   */
  const fun = (_treeData, parentTreeNode) => {
    _treeData.forEach((treeNode) => {
      // 如果需要,为每个树节点设置私有键
      isSetPrivateKey && setPrivateKey(treeNode, parentTreeNode, config);
      // 将当前树节点推入结果数组中
      result.push(treeNode);
      // 递归地遍历当前树节点的子节点(如果有的话)
      if (treeNode[childrenName]) {
        fun(treeNode[childrenName], treeNode);
      }
    });
  };
​
  // 从树形结构的根节点开始递归遍历
  fun(treeData);
​
  return result;
};

处理多维树形结构的函数方法

在开始的基础介绍中我们有提到过一维树形结构 有助于我们对数据进行增删改查。因为一维的树形结构可以很容易的使用的我们数组内置的一些 find filter map 等方法。这几个方法不知道小伙伴赶紧去补一补这些知识吧 看完了再回到这里。传送门

下面我们会介绍 findTreeDatafilterTreeDatamapTreeData 这三个方法。使用方式基本和find filter map原始数组方法一样。也有些许不一样的地方:

  1. 因为我们不是直接把方法绑定在原型上面的 所以不能直接 arr.findTreeData 这样使用。需要findTreeData (arr) 把多维树形结构数组当参数传进来。
  2. callBack函数参数返回有些许不同 。前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined。
  3. filterTreeDatamapTreeData方法我们可以通过配置项中的isTileArray属性来设置返回的是一维树形结构还是多维树形结构

findTreeData

/**
 * 筛选多维树形结构 返回查询到的第一个结果
 * @param treeData 树形结构数组
 * @param callBack 写入搜索条件 用法和数组的find方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
 * @param config 配置项(key,childrenName,isSetPrivateKey)
 * @returns 返回查询到的第一个结果
 */
const findTreeData = (treeData = [], callBack, config, parentTreeNode) => {
  // 定义配置项中的 childrenName 和 isSetPrivateKey 变量, 如果没有传入 config 则默认值为 {}
  const { childrenName = `children`, isSetPrivateKey = false } = config || {};
​
  // 遍历树形数据
  for (const treeNode of treeData) {
    // 当 isSetPrivateKey 为真时,为每个节点设置私有变量
    if (isSetPrivateKey) {
      setPrivateKey(treeNode, parentTreeNode, config);
    }
    // 如果 callBack 返回真, 则直接返回当前节点
    if (callBack?.(treeNode, treeData.indexOf(treeNode), parentTreeNode)) {
      return treeNode;
    }
    // 如果有子节点, 则递归调用 findTreeData 函数, 直到找到第一个匹配节点
    if (treeNode[childrenName]) {
      const dataInfo = findTreeData(treeNode[childrenName], callBack, config, treeNode);
      if (dataInfo) {
        return dataInfo;
      }
    }
  }
};

filterTreeData

/**
 * 筛选多维树形结构 返回查询到的结果数组
 * @param treeData 树形结构数组
 * @param callBack 写入搜索条件 用法和数组的filter方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
 * @param config  配置项(key,childrenName,isTileArray,isSetPrivateKey)
 * @returns 返回查询到的结果数组
 */
const filterTreeData = (treeData = [], callBack, config) => {
  const { childrenName = `children`, isTileArray = false, isSetPrivateKey = false } = config || {}; // 解构配置项
  const resultTileArr = []; // 用于存储查询到的结果数组
  const fun = (_treeData, parentTreeNode) => {
    return _treeData.filter((treeNode, index) => {
        if (isSetPrivateKey) {
          setPrivateKey(treeNode, parentTreeNode, config); // 为每个节点设置私有键名
        }
        const bool = callBack?.(treeNode, index, parentTreeNode)
        if (treeNode[childrenName]) { // 如果该节点存在子节点
          treeNode[childrenName] = fun(treeNode[childrenName], treeNode); // 递归调用自身,将子节点返回的新数组赋值给该节点
        }
        if (bool) { // 如果传入了搜索条件回调函数,并且该节点通过搜索条件
          resultTileArr.push(treeNode); // 将该节点添加至结果数组
          return true; // 返回true
        } else { // 否则,如果该节点存在子节点
          return treeNode[childrenName] && treeNode[childrenName].length; // 判断子节点是否存在
        }
      });
  };
  const resultArr = fun(treeData); // 调用函数,返回查询到的结果数组或整个树形结构数组
  return isTileArray ? resultTileArr : resultArr; // 根据配置项返回结果数组或整个树形结构数组
};

mapTreeData

/**
 * 处理多维树形结构数组的每个元素,并返回处理后的数组
 * @param treeData 树形结构数组
 * @param callBack 写入搜索条件 用法和数组的map方法一致(前两个参数一样 第三个参数为旧的父级详情 第四个是新的父级详情)
 * @param config  配置项(key,childrenName,isTileArray,isSetPrivateKey)
 * @returns 返回查询到的结果数组
 */
const mapTreeData = (treeData = [], callBack, config) => {
  const { childrenName = `children`, isTileArray = false, isSetPrivateKey = false } = config || {}
  const resultTileArr = []
  const fun = (_treeData, oldParentTreeNode, newParentTreeNode) => {
    return _treeData.map((treeNode, index) => {
      isSetPrivateKey && setPrivateKey(treeNode, oldParentTreeNode, config)
      const callBackInfo = callBack?.(treeNode, index, oldParentTreeNode, newParentTreeNode)
      if (isTileArray) {
        resultTileArr.push(callBackInfo)
      }
      const mappedTreeNode = {
        ...treeNode,
        ...callBackInfo,
      }
      if (treeNode?.[childrenName]) {
        mappedTreeNode[childrenName] = fun(treeNode[childrenName], treeNode, mappedTreeNode)
      }
      return mappedTreeNode
    })
  }
  const resultArr = fun(treeData)
  return isTileArray ? resultTileArr : resultArr
};

ts版本代码

/**
 * 操控树形结构公共函数方法
 * findTreeData     筛选多维树形结构 返回查询到的第一个结果
 * filterTreeData   筛选多维树形结构 返回查询到的结果数组
 * mapTreeData      处理多维树形结构数组的每个元素,并返回处理后的数组
 * getTreeData      一维树形结构转多维树形结构
 * flattenTreeData  多维树形结构转一维树形结构
 *//** 配置项 */
type TreeDataConfig = {
  /** 唯一标识 默认是id */
  key?: string
  /** 与父节点关联的唯一标识 默认是pId */
  parentKey?: string
  /** 查询子集的属性名 默认是children */
  childrenName?: string
  /** 返回值是否为一维树形结构  默认是false*/
  isTileArray?: boolean
  /** 是否添加私有变量 默认是false */
  isSetPrivateKey?: boolean
}
​
type TreeNode = {
  _pId?: string | number
  _pathArr?: Array<string | number>
}
​
/**
 * 新增业务参数。
 * _pId     父级id
 * _pathArr 记录了从一级到当前节点的id集合。
 * _pathArr 的length可以记录当前是多少层
 * @param treeNode
 * @param parentTreeNode
 * @param treeDataConfig
 */
const setPrivateKey = <T extends { [x: string]: any }>(
  treeNode: T & TreeNode,
  parentTreeNode: (T & TreeNode) | undefined,
  config?: TreeDataConfig
) => {
  const { key = `id` } = config || {}
  treeNode._pId = parentTreeNode?.[key]
  treeNode._pathArr = parentTreeNode?._pathArr
    ? [...parentTreeNode._pathArr, treeNode[key]]
    : [treeNode[key]]
}
​
type FindTreeData = <T extends { [x: string]: any }>(
  treeData?: readonly T[],
  callBack?: (treeNode: T, index: number, parentTreeNode?: T) => boolean,
  config?: TreeDataConfig,
  parentTreeNode?: T
) => (T & TreeNode) | undefined
/**
 * 筛选多维树形结构 返回查询到的第一个结果
 * @param treeData 树形结构数组
 * @param callBack  写入搜索条件 用法和数组的find方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
 * @param config  配置项(key,childrenName,isSetPrivateKey)
 * @returns  返回查询到的第一个结果
 */
export const findTreeData: FindTreeData = (treeData = [], callBack, config, parentTreeNode) => {
  const { childrenName = `children`, isSetPrivateKey = false } = config || {}
  for (const treeNode of treeData) {
    isSetPrivateKey && setPrivateKey(treeNode, parentTreeNode, config)
    if (callBack?.(treeNode, treeData.indexOf(treeNode), parentTreeNode)) {
      return treeNode
    }
    if (treeNode[childrenName]) {
      const dataInfo = findTreeData(treeNode[childrenName], callBack, config, treeNode)
      if (dataInfo) {
        return dataInfo
      }
    }
  }
}
​
/**
 * 筛选多维树形结构 返回查询到的结果数组
 * @param treeData 树形结构数组
 * @param callBack 写入搜索条件 用法和数组的filter方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
 * @param config  配置项(key,childrenName,isTileArray,isSetPrivateKey)
 * @returns 返回查询到的结果数组
 */
export const filterTreeData = <T extends { [x: string]: any }>(
  treeData: readonly T[] = [],
  callBack?: (treeNode: T, index: number, parentTreeNode?: T) => boolean,
  config?: TreeDataConfig
): (T & TreeNode)[] => {
  const { childrenName = `children`, isTileArray = false, isSetPrivateKey = false } = config || {}
  const resultTileArr: T[] = []
  const fun = (_treeData: readonly T[], parentTreeNode?: T): T[] => {
    return _treeData.filter((treeNode, index) => {
      isSetPrivateKey && setPrivateKey(treeNode, parentTreeNode, config)
      const bool = callBack?.(treeNode, index, parentTreeNode)
      if (treeNode[childrenName]) {
        ;(treeNode[childrenName] as T[]) = fun(treeNode[childrenName], treeNode)
      }
      if (bool) {
        resultTileArr.push(treeNode)
        return true
      } else {
        return treeNode[childrenName] && treeNode[childrenName].length
      }
    })
  }
  const resultArr = fun(treeData)
  return isTileArray ? resultTileArr : resultArr
}
​
/**
 * 处理多维树形结构数组的每个元素,并返回处理后的数组
 * @param treeData 树形结构数组
 * @param callBack 写入搜索条件 用法和数组的map方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
 * @param config  配置项(key,childrenName,isTileArray,isSetPrivateKey)
 * @returns 返回查询到的结果数组
 */
export const mapTreeData = <T extends { [x: string]: any }>(
  treeData: readonly T[] = [],
  callBack?: (
    treeNode: T,
    index: number,
    oldParentTreeNode?: T,
    newParentTreeNode?: T
  ) => { [x: string]: any } | any,
  config?: TreeDataConfig
): Array<T & TreeNode & { [x: string]: any }> => {
  const { childrenName = `children`, isTileArray = false, isSetPrivateKey = false } = config || {}
  const resultTileArr: Array<T & { [x: string]: any }> = []
  const fun = (_treeData: readonly T[], oldParentTreeNode?: T, newParentTreeNode?: T) => {
    return _treeData.map((treeNode, index) => {
      isSetPrivateKey && setPrivateKey(treeNode, oldParentTreeNode, config)
      const callBackInfo = callBack?.(treeNode, index, oldParentTreeNode, newParentTreeNode)
      if (isTileArray) {
        resultTileArr.push(callBackInfo)
        return
      }
      const mappedTreeNode = {
        ...treeNode,
        ...callBackInfo,
      }
      if (treeNode?.[childrenName]) {
        mappedTreeNode[childrenName] = fun(treeNode[childrenName], treeNode, mappedTreeNode)
      }
      return mappedTreeNode
    })
  }
  const resultArr = fun(treeData)
  return isTileArray ? resultTileArr : resultArr
}
​
/**
 * 一维树形结构转多维树形结构
 * @param tileArray 一维树形结构数组
 * @param config 配置项(key,childrenName,parentKey,isSetPrivateKey)
 * @returns 返回多维树形结构数组
 */
export const getTreeData = <T extends { [x: string]: any }>(
  tileArray: readonly T[] = [],
  config?: TreeDataConfig
): (T & TreeNode)[] => {
  const {
    key = `id`,
    childrenName = `children`,
    parentKey = `pId`,
    isSetPrivateKey = false,
  } = config || {}
  const fun = (parentTreeNode: { [x: string]: any }) => {
    const parentId = parentTreeNode[key]
    const childrenNodeList: T[] = []
    copyTileArray = copyTileArray.filter(item => {
      if (item[parentKey] === parentId) {
        childrenNodeList.push({ ...item })
        return false
      } else {
        return true
      }
    })
    parentTreeNode[childrenName] = childrenNodeList
    childrenNodeList.forEach(item => {
      isSetPrivateKey && setPrivateKey(item, parentTreeNode, config)
      fun(item)
    })
  }
  const rootNodeList = tileArray.filter(item => !tileArray.some(i => i[key] === item[parentKey]))
  const resultArr: (T & TreeNode)[] = []
  let copyTileArray = [...tileArray]
  rootNodeList.forEach(item => {
    const index = copyTileArray.findIndex(i => i[key] === item[key])
    if (index > -1) {
      copyTileArray.splice(index, 1)
      const obj = { ...item }
      resultArr.push(obj)
      isSetPrivateKey && setPrivateKey(obj, undefined, config)
      fun(obj)
    }
  })
  return resultArr
}
​
/**
 * 多维树形结构转一维树形结构
 * @param treeData 树形结构数组
 * @param config 配置项(key,childrenName,isSetPrivateKey)
 * @returns 返回一维树形结构数组
 */
export const flattenTreeData = <T extends { [x: string]: any }>(
  treeData: readonly T[] = [],
  config?: TreeDataConfig
): (T & TreeNode)[] => {
  const { childrenName = `children`, isSetPrivateKey = false } = config || {}
  const result: T[] = []
  const fun = (_treeData: readonly T[], parentTreeNode?: T) => {
    _treeData.forEach(treeNode => {
      isSetPrivateKey && setPrivateKey(treeNode, parentTreeNode, config)
      result.push(treeNode)
      if (treeNode[childrenName]) {
        fun(treeNode[childrenName], treeNode)
      }
    })
  }
  fun(treeData)
  return result
}
​