Tree-chain 提供处理树形数据可视区渲染的方案

307 阅读6分钟

前言

树形数据一直以来都是在数据处理上最复杂的一类数据之一,即使是目前市场最常见的功能比如筛选、排序、分页等,也同样有着不小的逻辑复杂度。

让我们试着想一下,即使是简单的对树形数据进行筛选,就有许许多多不同的策略选择,比如父节点和子节点的存在关系,有的时候是父节点被筛除把对应的所有子节点(包括孙子节点)筛除,有的时候是子节点被筛留时即使祖先节点被筛除也要保留所有的祖先节点;又比如对树形数据的排序,需要对每一层节点进行遍历排序。倘若筛选、排序的条件过于复杂,整个算法的复杂度也会指数级上升。

而对于树形数据的渲染优化,也很难有比较舒服的通用解决方案。

lft-rgt.jpg

树形数据的渲染

一份树形数据的功能基本避不开这几点功能,数据展开和收起、对节点进行编辑或者移除、对节点进行移动。在 PC 中,最常见的树形数据就是资源管理器文件列表,当中打开文件夹、新建文件(夹)、移动文件(夹)都是对文件树节点进行处理。

优化的方式有哪些?

通常来讲,树形数据往往都是用列表或者表格去渲染,如果遇到大规模的数据量,我们可以采取分页的形式去加载这类数据,但对于树形数据来说,即使分页也很难做到完美的解决数据量问题。

究其原因就是分页往往只能对某一层节点进行分页,我们根本无从得知某一个节点下有多少个子孙节点,比如分页一页只加载十条数据,但是某一条数据下又可能有上万条子数据,甚至某一条子数据下还有更多的子数据,这时候分页就无法解决不了渲染优化的问题了。除非对每一层节点都做上分页功能,但这样对用户来说体验实在是太差了。

而除了分页去优化渲染,那还有一条路可走,那就是目前在列表或者表格渲染优化上最优秀的设计——可视区渲染,也可以叫做虚拟渲染技术。

可视区渲染的基本原理

简单来说可视区渲染的原理是通过捕捉列表当前用户可以看到的区域,然后结合滚动条高度去计算渲染的起始数据和终点数据,之后只渲染这部分数据并通过空白块去模拟未渲染的数据需要占用的空间,这条对于用户来说,就和正常的列表渲染感观相差无几,同时又能优化渲染性能,是绝佳的列表优化方案。

image.png

但是这些原理只对列表数据适配良好,因为列表我们可以直接用切片(slice)的方式把当中的某一块数据切出来,而树形数据明显并不具备这样的条件,树形数据的节点并不是固定且连续的,结合相关的业务场景,一个节点的下一个节点究竟是它的兄弟节点还是它的第一个子节点,还是下一棵树的头部节点,这都不是一个直观上就能直接得出的答案。

Tree-chain 的用途

为了解决可视区渲染难以支持树形数据的问题,我就写了这样一个数据结构 tree-chain

image.png

这个库的方案是在树形数据中模拟这份数据数组渲染的顺序,用链表的形式串连节点,同时提供了解决一系列业务常见需求的方法。

在将一份树形数据通过 tree-chain 实例化后,我们可以通过提供的一些方法对这个树形数据的节点进行增删和移动,最后调用 toArray 方法生成想要的可视区域数组,这个方法支持了在将树形链表数据转化成数组时进行各种各样的筛选和排序,如同下面所示代码:

import { TreeChain } from 'tree-chain'

const MOCK_DATA: Data[] = [
    { id: 0, name: "tom", children: [
        { id: 1, name: "jerry" },
        { id: 2, name: "john" },
    ]},
    { id: 3, name: "halo", children: [
        { id: 4, name: "rox", children: [
            { id: 5, name: "holy", children: [
                { id: 6, name: "xekin" }
            ]},
            { id: 7, name: "siri" },
            { id: 8, name: "san" },
            { id: 9, name: "dan", children: [
                { id: 10, name: "danny" },
                { id: 11, name: "ally" },
            ]}
        ]},
        { id: 12, name: "neo" }
    ]},
    { id: 13, name: "belly" },
];

const treeChain = TreeChain.create(MOCK_DATA);

const arr = treeChain.toArray({
    // 筛选
    filter: (node) => ![6, 9, 10].includes(Number(node.key)),
    // 筛选时子节点存在时祖先节点是否保留
    keepAncestorsWithChildren: true,
    // 筛选时子节点全被筛除时父节点是否保留
    keepParentWithoutChildren: false,
    // 排序
    sort: (a, b) => Number(b.key) - Number(a.key),
    // 起点的 key
    startKey: 5,
    // 数量
    count: 5,
});

arr.map((node) => node.key.toString());
// [12,9,11,8,7];

这个库是纯 Typescript 实现的,基本实现了全部的数据类型自动推导,而且我也覆盖了单元测试,可以放心食用。

Tree-chain 的原理

顾名思义,我就是将整个树形数据的每个节点进行了串连,这一点其实跟 react-fiber 的数据结构类似,为每个节点赋予了其渲染顺序的前后节点的指针以及兄弟节点的指针,并实现了一系列节点增删、移动、插入后并维持结构的方法。其难点是在于实现一些节点移动的方法以及筛选和排序,要在做了筛选和排序后,保持数组数据渲染顺序的合理性。

这个库处理的是多个树形数据联结的数组,而不是单单一个具有 top 节点的树形数据,当然内部有许多实例方法,也可以处理更多种场景情况。功能使用可以参照这里 测试用例

tree-chain 的节点顺序

参照前面的结构图,tree-chain 当中的前后节点链表顺序并不是只串连了某一层,或者父子之间。而是对整份树形数据数组的所有节点按渲染顺序进行串连。这个顺序是整个数据结构的关键所在,而一个节点的下一个节点可能有好几种情况,例如:

  • 同层的下一个节点
  • 这个节点的第一个子节点
  • 某一个祖先节点的下一个兄弟节点
  • 下一棵树的第一个节点

最后

这是 tree-chain 代码仓库, npm 包名就叫 tree-chain, 希望各位老板能够来多提提意见咯。

这里是 Xekin(/zi:kin/),以上这就是本篇文章分享的全部内容了,喜欢的掘友们可以点赞关注点个收藏~

最近摸鱼时间比较多,写了一些奇奇怪怪有用但又不是特别有用的工具,不过还是非常有意思的,之后会一一写文章分享出来,感谢各位支持。

我还是喜欢写没人写过的东西~