将含有层级关系的一维数组转为上下级依赖的树状对象
当开发过程中,遇到一组有层级关系的一维数组对象,需要将其处理为有上下级依赖关系的树状对象,遇到这个场景可以将本文的内容做参考使用。
场景示例
本文内容只适合处理像以下有清晰简明的数据结构,复杂凌乱的结构是不支持的。
现在有一份一维数组数据,其中每项有一个属性代表着其级别(权重),其数据如下:
js
const list = [
{
name: '目录',
weight: 1
},
{
name: '导航一',
weight: 2
},
{
name: '导航-路由一',
weight: 3
},
{
name: '导航-路由二',
weight: 3
},
{
name: '导航二',
weight: 2
},
{
name: '导航二-路由',
weight: 3
}
]
现在的诉求是,将list
数组转为树状对象,并能正确的表达其上下级关系。期望结果为:
js
const result = {
name: '目录',
weight: 1,
children: [
{
name: '导航一',
weight: 2,
children: [
{
name: '导航-路由一',
weight: 3,
children: []
},
{
name: '导航-路由二',
weight: 3,
children: []
},
]
},
{
name: '导航二',
weight: 2,
children: [
{
name: '导航二-路由',
weight: 3,
children: []
}
]
}
]
}
若是你也有上面场景需求,那就向下划拉划拉吧。
这个场景的使用举一个简单例子:
需要对md文件的h标签内容作为目录或者锚点信息使用。比如掘金是怎么处理md的h标签转为右侧目录的。虽不知具体逻辑,但读取md内容,解析h标签内容,转为层级结构这个过程肯定不会少的。当然这个过程也能使用一些md相关库辅助快速实现。
接下来展开聊下具体思路与代码实现:
思路
首先分析这个list
,能直观感受到的就是weight
属性是破局的关键点。
但怎么能让它顺顺利利的听话,还需要设置一些规则才行。
比如:
weight
在循环过程中若是遇到相同或小的值,需要结束循环,因为后面的就属于下一个关系了。weight
需要在循环过程中有一个参考项,这个参考项告诉它是否越界了。
完整代码
js
// 关系解析器 - 适用于关系清晰的数组结构,而且只存在一个最高级,通常是第一项。
class RelationshipParser {
#dataSource; // 源数据
#result; // 处理的结果
/**
*
* @param {array} dataSource // 一维数组源数据, 没用ts编写的话,尽量校验下dataSource
*/
constructor(dataSource) {
this.#dataSource = JSON.parse(JSON.stringify(dataSource));
this.#init();
}
getResult() {
return JSON.parse(JSON.stringify(this.#result))
}
#init() {
const topLevelItem = this.#getTopLevel(); // 通常只有一个最高级
this.#parseData(this.#dataSource, topLevelItem);
this.#result = topLevelItem;
}
#getTopLevel() {
const topValue = Math.min(...this.#dataSource.map(item => item.weight));
return this.#dataSource.find(item => item.weight === topValue);
}
/**
* 递归解析当前dataSource,组成依赖关系树
* @param {array} dataSource // 源数据
* @param {object} currentItem // 当前节点,parseData函数处理的都是他的子级
*/
#parseData(dataSource, currentItem) {
currentItem.children = [] // 这个children就是容器,可以修改
// 起始索引,用于循环使用
const startIndex = dataSource.findIndex(item => item.weight === currentItem.weight);
// 当前权重,用于划分当前边界
const currentWeight = currentItem.weight;
// 边界,用于划分当前命中范围
let boundaryDepth = Number.MAX_SAFE_INTEGER;
// 这里startIndex + 1作为起始索引,是为了只处理currentItem后面的数据
for (let index = startIndex + 1; index < dataSource.length; index++) {
const item = dataSource[index];
// 若当前权重小于等于入参权重,则跳出循环。
// 如 weigit:3 = weigit: 3, 说明是同级
// 如 weigit:2 < weigit: 3, 说明没有关系
// 如 weigit:4 > weigit: 3, 说明是嵌套关系,继续向下处理
if (item.weight <= currentWeight) {
break;
}
// 若当前权重小于等于边界权重,其实就是不是同一个权重就不处理
// 如 weigit:2 < weight: 10000,说明是第一次命中,将当前项push到 currentItem 内
// 只有第一次是小于,后面只会处理等于,因为小于在上一拦截了,大于就是越界了不做处理
if (item.weight <= boundaryDepth) {
// 递归处理当前项的子级
this.#parseData(dataSource.slice(index), item);
// 将当前项push到currentItem
currentItem.children.push(item);
boundaryDepth = item.weight;
}
}
}
}
看下使用效果:
js
// 列表
const list = [
{
name: '目录',
weight: 1
},
{
name: '导航一',
weight: 2
},
{
name: '导航-路由一',
weight: 3
},
{
name: '导航-路由二',
weight: 3
},
{
name: '导航二',
weight: 2
},
{
name: '导航二-路由',
weight: 3
}
]
// 调用
const relationshipParser = new RelationshipParser(list);
console.log(relationshipParser.getResult());
// => 如下
{
"name":"目录",
"weight":1,
"children":[
{
"name":"导航一",
"weight":2,
"children":[
{
"name":"导航-路由一",
"weight":3,
"children":[
]
},
{
"name":"导航-路由二",
"weight":3,
"children":[
]
}
]
},
{
"name":"导航二",
"weight":2,
"children":[
{
"name":"导航二-路由",
"weight":3,
"children":[
]
}
]
}
]
}