笔者在工作中遇到了一个web环境需要展示100w级目录节点treeview的需求,本文重点介绍笔者设计的一种treeView分页的方法。
1、无限滚动长列表
前端的业务开发中会遇到数量很大的列表展示情况,一般的处理方法是使用某种方法分屏分页的加载数据。
通常的做法是检测是否滚动到底,然后进行网络请求操作。
const maxScrollTop = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight;const currentScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
if (maxScrollTop - currentScrollTop < 20) { //… getNext}
一般情况下,这种方法已经够用。
但是,万一,列表的每一项结构复杂,用户又有可能上下滚动一整天呢?
我们来稍微测试下:
let start = new Date(); let imgUrl = 'http://127.0.0.1/profile.jpg'; let containerDom = document.getElementById('container'); const count = 100000; var array = []; for (var i = 0; i < count; i ) { let html = `<div id=item-${i}> <p>第${i}个元素的title</p> <div> <img src=${imgUrl}?index=${i}> </div> <div><div>嘿嘿</div><div>呵呵</div><div>啦啦</div></div> </div>` array.push(html); } containerDom.innerHTML = array.join(''); setTimeout(function() { console.log(new Date() - start); }, 0);
当count=100时:
内存占用空间 |
---|
37992k |
当count=1000时:
内存占用空间 |
---|
93152k |
当count=100000时:
内存占用空间 |
---|
2741972k |
如此简单的dom节点结构当有100000在dom树中都会占用如此巨大的内存,导致页面卡顿严重。真实项目中,dom节点结构往往复杂的多。
2、基于dom复用的长列表实现
针对dom元素过多的问题,我们使用dom复用的思想优化。
思路是不完整渲染所有元素,只对「可见区域」进行渲染。
- 可滚动区域:假设有 1000 条数据,每个列表项的高度是 30,那么可滚动的区域的高度就是 1000 * 30。当用户改变列表的滚动条的当前滚动值的时候,会造成可见区域的内容的变更。
- 比如列表的高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可见区域。
- 计算当前可见区域起始数据的 startIndex
- 计算当前可见区域结束数据的 endIndex
- 计算当前可见区域的数据,并渲染到页面中
- 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
3、基于dom复用思想实现的treeview
treeview我们可以理解为需要展示树形结构的listview。
我们可以基于listview实现它。
具体实现过程不是本文的重点。
4、一种TreeView组件分页异步加载的方法
本文的重点是介绍一种TreeView组件分页异步加载的方法。
遇到的问题:
Treeview是一级一级展开的,最开始让人自然而然的想到,每次展开的时候我们发出网络请求,然后更新组件。
这个时候就有问题了:如果当展开一个节点的时候,此节点的子节点有无限多个,怎么办呢?先不说treeview组件顶不顶的住。甚至都有可能超过单次http请求的最大长度限制。
我们自然而然的觉得应该分页。
但是树形结构不像listView、gridView等线性结构那样,可以很方便的分页,树形结构的分页,配上树节点的展开收起状态,想想都复杂,怎么办呢?
treeview还支持从任一个节点进入,并且每一层的节点还是有序的。这让分页方案会更加复杂。
解决方案1:
所有展开收起状态存在服务端,后端通过前端传递的每条item的高度,每条item的上下间距,当前滚动的距离,返回相应的信息,前端只有很薄的显示计算逻辑。
这样理论上是可行的,但考虑到前端可以任意滚动,并且后端的逻辑会很复杂。所以我们还是暂不考虑。
解决方案2:
我把他总结成视图层向外索要数据。
第一步:视图数据层建出空树:进入节点,先拉到直接子节点count,在treeView的数据层该节点下新建一个count长的空Array。如果进入节点不是树的根节点,则向该进入节点的祖先回溯,并用同样的方法建出只有关键节点的空树。由于我们的树的每一层都是有序的,则还需要去服务器拉出该节点在父节点的子节点中的位置。
第二步:视图层向外抛出索要数据事件:这个时候treeview要开始渲染了,第一次渲染,发现实际数据为空,则视图上先以空样式占位,同时抛出事件,告知控制器需要加载数据的父节点以及startIndex与endIndex。
第三步:组件控制器发出网络请求,帮助视图层完善树形结构:维护一个队列,控制同时发出的网络请求数量,避免快速滚动下发出过多网络请求的问题。
维护一个网络请求队列,使用生产者消费者模式去消费队列。被略过的网络请求promise将被手动cancel掉,成功执行的promise 将会在resolve中更新treeview的数据源并且让treeview二次渲染。
以下视频是这套方案的实现效果,如果网络条件不错的话几乎看不到第一次渲染造成的空元素占位现象。