树组件
新的树组件解决的问题:
- 同时支持单选和多选,
- 支持同步异步加载
- 解决了大数据量下,以及异步展开数据过多的性能问题,可以支持20万级以内的数据渲染,而无明显体验的下降(注:无超大部门,比如一个部门有2000个以上的部门,会造成初次查询性能下降)
- 支持自定义渲染,可以自由控制节点的样式和行为
新树组件的实现思路
基于React,无hack和cheat, 虚拟化渲染,平级数据结构, 内部数据缓存,全受控模式, 方便不同场景下对接的灵活性。可适配各种使用树的业务场景。
文档是帮自己理一下思路。
关于树组件的数据格式
渲染树组件的过程中,嵌套结构更符合UI的结构,但是这种结构最大的问题是,子操作对父的影响不好追踪和控制,所以要实现嵌套结构和实现子影响父的操作,需要在id上加上路径。否则就很难实现了。
平级的数据格式更方便掌握整体的数据状态,以及数据间的从属关系。
对于大量数据的处理,最大的问题是数据的查找,尤其是数组结构的数据,每次定位一个数据项都,都需要做一次遍历,根据操作方式的不同,相当一部分操作可能需要完全遍历整个数组。对于目前的电脑的性能来说,几万,几十万条数据可能问题不大。但是作为程序始终要考虑极端情况。
对于渲染一个树来说,这个树上的节点始终要遍历一遍,尤其是平级结构还要判断从属关系。所以在遍历的过程中,对于已经查到结果的数据,把数据再缓存成更方便查找的Map格式,会极大的提升后续操作的效率,但是需要在组件的生命周期内在计算机里占据一定的资源。但会提升用户操作过程中的体验。你也不想点击一个checkbox,得等2s这个checkbox才会被选上吧。
性能
非虚拟化渲染的树组件在一定量之内,性能没太大问题,但是超过一定量,比如万以上的节点数量,性能下降很明显,这个性能下降主要原因并不是dom节点的量,而是通过react构建整个虚拟dom过程中的操作产生的内部运算,这个在常规方案里基本无解。你可以试一下,几万个节点都过原生dom操作插入并显示出来,也就是不到1s的事情。但是原生dom操作,数据状态管理会变的复杂无比。你就会体会到有个像react这样的库到底有多幸福。
关于缓存
缓存数据的目标,
- 对于已经定位到的数据,可以不通过遍历快速找出来
- 缓存可以描述数据之间的关系,方便关联操作。
虚拟化渲染
刚才说过了,用react渲染大量的数据,就是有问题,而通过原生dom的方式,数据管理成本太高。那么应该怎么解决这个问题,经过搜索,和身边朋友的建议,虚拟化渲染是个不错的方案。首先可以充分利用react本身的优势,又避免了原生dom方式增加的数据状态管理的成本。
那么虚拟化渲染的意思是什么呢,就是只渲染用户可以看到的部分,剩下的都不渲染,用户看到哪里渲染到哪里,保证一次处理的数据只限于可视区域中的部分。这样计算的成本,和渲染的成本就变的非常小了。
调研过程
失败案例1
最初猜测的是数据操作导致的性能问题,然后尝试平级数据结构+缓存的方式来渲染列表,在1000以内的情况下,速度还可以,但是到万级别以之后,就发现事情不是这个样子,所以测试了一下数据操作和React渲染的时间占比,发现数据操作10万以上快如兔,但是React的渲染就慢如猪了。不管你采用多少性能优化的策略,但是在react中,只要节点到达一定的量之后,他就是这样。
失败案例2
既然使用React渲染不行,测试使用原生dom渲染大量节点的性能,发现非常快,那么是不是可以换个思路用原生Dom来实现呢,其实可以。用原生dom直接渲染2万个节点,速度是非常快,1s左右吧,但是用原生dom来做这个事情,会发现,数据状态同步和事件处理成了问题,没有virtualDom,没有SytheticEvent,随之而来的复杂度瞬间就起飞了。一切状态同步都需要手动处理,问题可以解决,但维护成本就太大了。
有同学会问,是不是出现这种情况意味着react就是失败的呢,肯定不是。
不完美案例3
react virtualize,虚拟化渲染,虚拟化渲染说白了就是看到哪些渲染哪些,在滚动的时间不停的更新dom节点,把整个问题从如何渲染上万个节点转化成如何知道应该渲染哪些可见的节点。于是通过react virtualize尝试了一把,D确可以解决问题,性能起飞了,10条数据和100万数据区别都瞬间渲染完成但是,问题就在于list的处理方式。对于vitualize渲染,需要知道List的根子节点的高度,但对于树来说,每个根子节点下面都有可能也是一颗子树,子树是不能虚拟化的。所以如果一颗子树太大,问题又回到了原点。所以这个方案不完美。而且react-virtualize这个库的体积太大了。也不利于整体项目的性能优化
最终方案
当前方案
运行示例
- clone本项目,
- npm install
- npm start
- 打开浏览器,输入localhost:3000进行
关于内部数据的处理
具体的数据格式请参考interface定义
单条数据项目格式
//节点数据格式,通过id, pid标识从属关系
export interface ITreeItem {
name: string;
id: TDataID;
pid: TDataID;
hasChildren?: boolean;
}
// 数据列表格式
export type ITreeList = ITreeItem[]
// Tree组件接受的参数
export interface ITreeProps {
height?: number;
width?: number;
data: TTreeList;
rootId: TDataID;
value: ITreeValue;
onChange: (value: ITreeValue) => void;
rowHeight?: number;
multi?: boolean;
renderItem?: (props: ITreeItemProps) => JSX.Element;
className?: string;
}