引言:上篇我们说了如何根据实际业务抽象表达式树,本篇我们就来讲讲树的操作在业务中的应用。
树的操作
树的操作有很多,根据基本的增删改查进行分类,结果如下:
增加操作:
- 插入节点:在树中插入一个新节点。
- 添加子节点:将一个节点作为另一个节点的子节点添加到树中。
删除操作:
- 删除节点:从树中删除指定的节点。
- 删除子树:删除一个节点及其所有子节点。
修改操作:
- 更新节点值:修改树中指定节点的值。
- 移动节点:将一个节点从一个位置移动到另一个位置。
查询操作:
- 查找节点:在树中搜索指定值的节点。
- 遍历树:按照特定顺序访问树中的所有节点。
- 获取子树:获取一个节点及其所有子节点组成的子树。
- 获取父节点:获取指定节点的父节点。
- 获取兄弟节点:获取指定节点的兄弟节点(具有相同父节点的节点)。
- 获取子节点:获取指定节点的所有子节点。
- 获取根节点:获取树的根节点。
- 判断节点是否为叶子节点:检查节点是否没有子节点。
- 判断节点是否为根节点:检查节点是否没有父节点。
- 判断节点是否为某节点的子节点:检查节点是否是指定节点的子节点。
- 判断节点是否为某节点的祖先节点:检查节点是否是指定节点的祖先节点。
从上面我们可以看出,树的操作有非常多种,但这里,我们只谈基于上篇的结构所涉及到的操作。
业务需求
接下来,我们从实际的业务需求出发,先对业务需求进行分析,然后拆解出针对树的操作,再实现实际代码。
根据原型,我们列出可能的操作:
1、切换交并类型
切换交并类型属于对节点的修改。
2、拖拽标签到组内
拖拽标签到组内,对标签节点属于添加节点,对类型标签属于修改节点。
3、增加或者删除组
属于添加、删除节点操作。
4、根据标签组合生成sql
这属于表达式树的遍历生成。
在开始实际代码之前,我们先根据上篇定义的数据结构,初始化一个嵌套对象。 数据结构:
typescript
interface ExpressionNode {
// 索引,保证唯一性
id: string
// 节点类型
type: 'inter'| 'union' | 'diff' | 'label'
// 节点的值
value: 'and' | 'or' | 'not' | LabelNode,
// 子节点,可选
children?: ExpressionNode[]
}
这里增加了一个 id 索引,保证每个节点的唯一性。 初始化对象为: 这里要说明的是,为了方便操作,我并没有存储最上层的 交集操作,而是将其存储为一个数组对象。 为了方便大家对照,再放一遍定义的表达式树:
typescript
const labelOps: ExpressionNode[] = [
{
id: '1',
// 此type为组间交并关系
type: 'union',
value: 'or',
children: [
{
id: '11',
// 此type为组内交并关系
type: 'union',
value: 'or',
children: [],
},
{
id: '12',
// 此type为组内交并关系
type: 'union',
value: 'or',
children: [],
},
],
},
{
id: '0',
// 此为差集
type: 'diff',
value: 'not',
children: [
{
id: '01',
// diff 内的标签是并集
type: 'union',
value: 'or',
children: [],
},
],
},
]
定义好了初始结构,我们就来按照所涉及的操作来写实现函数。
1、切换交并类型
切换交并分为组间的交并类型切换,和组内的交并类型切换。
组间的交并类型切换是直接改变数组第一项的type和value 类型。 我们可以定义函数如下:
typescript
const valueMap = {
'union': 'or',
'inter': 'and',
'diff': 'not',
}
const onClickGroupType = (type)=>{
labelOps[0].type = type;
labelOps[0].value = valueMap[type]
}
上面只是一个改变的示例,在实际的代码中,由于这个节点的值跟页面样式直接相关,所以,这里的type是直接和dom绑定的。
2、增加或者删除组
将对象转化为数组之后,处理就方便多了。因为我们这里只有两层结构,不同的删除只需要去判断不同的层,然后通过filter 进行过滤就好了。 这里我简单列下代码:
typescript
// jsx 代码
{uniInterFilter.value.children.map((group, index) => (
<div class="group-item" key={group.id}>
<div class="flex-space-between">
<span>
<span class="title btn-right-12">组合{index + 1}</span>
{index > 1 && <DeleteOutlined style="color: #8594AD;" onClick={() => clearGroupLabel(index)} />}
</span>
{/* 组内交并差 */}
<a-radio-group v-model:value={group.type} button-style="solid" size="small">
<a-radio-button value="union">并集</a-radio-button>
<a-radio-button value="inter">交集</a-radio-button>
</a-radio-group>
</div>
</div> )))
// 函数
// 响应点击删除当前组合
const clearGroupLabel = (delIndex: number) => {
uniInterFilter.value.children = uniInterFilter.value.children.filter((item, curIndex) => curIndex !== delIndex)
}
3、拖拽标签到组内
拖拽标签也是利用了draggable 第三方库,直接绑定数组到上面完成的。
4、根据标签组合生成sql
如果是一个二叉树,那么这个遍历过程用中序来做最合理,但是由于这是一个多叉树,所以通过深度遍历的方式完成,在将每个子节点处理成sql后,再根据当前的type类型进行组合。
typescript
/** 将交 or 并 or 差集标签列表转为sql */
export const getTreeSql = (type: 'ck' | 'hive', labelOp: LabelExpressionNode): string => {
// 如果是标签的话,直接将标签转为sql
if (labelOp.type === 'label') return labelToSql(type, labelOp.value!)
const labelSqlList = labelOp.children
.map((child) => getTreeSql(type, child))
.filter(Boolean) // 过滤掉空的标签或数组
if (!labelSqlList.length) return ''
// 根据当前的type类型进行组合,如果存在多个组合的时候,就添加外括号
return `${labelSqlList.length > 1 ? `(${labelSqlList.join(valueMap[labelOp.type])})` : labelSqlList.join(valueMap[labelOp.type])}`
}
在实际需求中,基本上所涉及到的相关树的操作就是以上。