背景
树状结构是一种十分常见的数据结构,比如,企业的部门树、文件系统的目录树等等,都是树的典型使用场景。虽然,这些很可能是完全不同领域的业务逻辑;但是,围绕树状结构的基本操作,可以说,都是差不多的。另一方面,由于树状结构相较而言,会存在更多的一些复杂性,比如深度不确定的嵌套关系,给用户界面展示、数据搜索、业务逻辑处理、以及前后端数据交互,增加了复杂度。
设计思路
引入一个一维数组,将树“拍扁”,即可大大简化树状结构的数据读取与操作。尤其,如果系统采用了关系型数据库来做持久化,其数据本身也是以类似于一维数组的方式保存在表中的。因此,可以将这种方式再扩展到前后端数据交互、以及前端数据读取与操作。
下面将分享,前端从后端接收到一维数组,如何转化成树的数据结构。
代码
后端接口提供的一维数组的元素定义如下:
type Item = {
id: number;
parent?: number; // 父节点ID
pos?: number; // 父节点下的兄弟数组中的下标值
// ...
}
利用Array.reduce方法实现一维数组转换成树状结构。生成的树状结构的元素将会增加两个成员属性:
1)path:节点的完整路径;
2)children:子节点数组。
另外,为了提高通用性,数组元素的定义中如果不是默认的id、parent、pos,可以通过config参数的fieldNames进行修改适配。还可以利用config的callback方法对每个节点进行额外的处理,比如,不同的UI组件库对显示字段的命名不一样(比如,有的叫title,有的叫label,有的叫name,等等),就可以在这里处理。
/** 转化成树之后的节点数据定义 */
export type TreeNodeType<T> = T & {
path: number[]; // 节点路径
children?: TreeNodeType<T>[]; // 子节点数组
};
interface ArrayToTreeConfig<T, V = T> {
parent: number;
fieldNames: { id?: string; parent?: string; pos?: string };
callback: ((value: TreeNodeType<T>) => TreeNodeType<V>) | undefined;
}
/** 一维数组转化树 */
export function arrayToTree<T, V = T>(
arr: T[],
config?: Partial<ArrayToTreeConfig<T, V>>,
) {
const {
id = 'id',
parent = 'parent',
pos = 'pos',
} = config?.fieldNames || {};
return loopArray(arr, [], {
parent: config?.parent || 0,
fieldNames: { id, parent, pos },
callback: config?.callback,
});
}
function loopArray<T, V = T>(
arr: T[],
path: number[],
config: Omit<ArrayToTreeConfig<T, V>, 'fieldNames'> & {
fieldNames: { id: string; parent: string; pos: string };
},
) {
const {
parent,
fieldNames: { id: idField, parent: parentField, pos: posField },
callback,
} = config;
return arr.reduce((prev, current, _) => {
if (((current as never)[parentField] || 0) === parent) {
const pos = (current as never)[posField] || 0;
(current as TreeNodeType<V>)['path'] = [...path, pos];
const children = loopArray(arr, (current as TreeNodeType<V>)['path'], {
...config,
parent: (current as never)[idField],
});
if (children.length > 0) {
(current as TreeNodeType<V>)['children'] = children;
}
prev[pos] = (
callback ? callback(current as TreeNodeType<T>) : current
) as never;
return prev;
}
return prev;
}, []) as TreeNodeType<V>[];
}