问题:
使用一般的tree组件渲染大量数据(如几千个树节点)的时候会非常卡顿,主要原因是页面中绘制的大量的Dom,滚动或展开、收起不断造成页面重绘、回流,使得性能不佳。
解决思路:
Step1:将树形数据拍平成一般的List
Step2:采用padding缩进的方式营造树形结构
Step3:在结合虚拟列表高效渲染长列表
?虚拟列表大致原理:当列表data中有n个item项,我们只渲染可视区域(比如10条)的item,页面滚动时获取到scrollTop,scrollTop / itemHeight = startIndex(当前滚动了多少条的索引),可视区域的数据 = data.slice(startIndex, startIndex + 10)),将可视区域数据渲染到页面即可。
数据说明:
列表项高度固定:itemHeight
列表数据:data,源数据
当前滚动位置:scrollTop
可视区域的数据:visibleData ?,就是你要真实渲染的数据
列表真实长度:itemHeight * data.length,制造滚动条
接着监听的scroll事件,获取滚动位置scrollTop
计算当前可视区域起始数据索引(startIndex = Math.floor(scrollTop / itemHeight) )
计算当前可视区域结束数据索引(endIndex = startIndex + visibleCount)
计算当前可视区域的数据 (visibleData ?= data.slice(startIndex,endIndex))
计算startIndex对应的数据在整个列表中的偏移量offset并设置到列表上
实际效果:
完整代码:
import React, { useCallback, useEffect, useRef, useState } from 'react';
import './index.css';
import { originData } from './mockData';
// 配置项
const options = {
defaultExpand: 1,
itemHeight: 30,
visibleCount: 15,
};
// 将树形数据转成普通列表数据(我用的是中国省-市-区的树形数据)
function flattenData() {
function flatten(tree, childKey = 'children', level, parent = null) {
let res = [];
tree.forEach((item) => {
item.level = level;
item.expand = level === 1;
item.parent = parent;
if (item.visible === undefined) {
item.visible = true;
}
if (!parent.visible || !parent.expand) {
item.visible = false;
}
res.push(item);
if (item[childKey] && item[childKey].length) {
res.push(...flatten(item[childKey], childKey, level + 1, item));
}
});
return res;
}
return flatten(originData, 'children', 1, {
level: 0,
visible: true,
expand: true,
value: '中国',
children: originData,
});
}
// 定义组件
function VirtualTree() {
// 如果是vue把data、visibleData...等定义在data() {}里,把setXxx定义在methods里即可
const treeRef = useRef();
const [data, setData] = useState([]);
const [visibleData, setVisibleData] = useState([]);
const [contentHeight, setContentHeight] = useState(10000);
const [offset, setOffset] = useState(0);
// 模拟获取接口数据
const getData = useCallback(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(flattenData());
}, 500);
});
}, []);
// 挂载时获取原始数据,相当于vue的 mounted()
useEffect(() => {
getData().then((res) => {
setData(res);
});
}, []);
// data变化更新可视数据,相当于vue的watch data
useEffect(() => {
if (data.length) {
updateVisibleData();
}
}, [data]);
// 可视数据变化更新容器高度, 相当于vue的watch visibleData
useEffect(() => {
setContentHeight(visibleData.length * options.itemHeight);
}, [visibleData]);
// 获取所有可视数据
function getAllVisibleData() {
return data.filter((item) => item.visible);
}
// 滚动页面时更新 visibleData、offset
function updateVisibleData(scrollTop = 0) {
const start = Math.floor(scrollTop / options.itemHeight);
const end = start + options.visibleCount * 2;
const allVisibleData = getAllVisibleData();
const _visibleData = allVisibleData.slice(start, end);
setVisibleData(_visibleData);
setOffset(scrollTop);
}
function handleScroll() {
const { scrollTop } = treeRef.current;
updateVisibleData(scrollTop);
}
function recursionVisible(children = [], status) {
children.forEach((node) => {
// 如果是折叠-->折叠所有子项; 如果是展开-->显示下一级
node.visible = status;
if (!status) {
node.expand = false;
}
if (node.children && node.children.length && !status) {
recursionVisible(node.children, status);
}
});
}
// 折叠与展开
function toggleExpand(item) {
const isExpand = !item.expand;
item.expand = isExpand;
recursionVisible(item.children, isExpand);
// 更新视图
handleScroll();
}
return (
<div ref={treeRef} className="tree" onScroll={handleScroll}>
{/* tree-phantom是用于制造滚动条 ,= 所有可视item的高度之和 */}
<div className="tree-phantom" style={{ height: contentHeight }}></div>
<div
className="tree-content"
style={{ transform: `translateY(${offset}px)` }}
>
{visibleData.map((item, index) => {
return (
<div
key={item.value + item.parent.value}
className="tree-list"
style={{
paddingLeft:
15 * (item.level - 1) + (item.children ? 0 : 15) + 'px',
height: options.itemHeight + 'px',
}}
>
{item.children && item.children.length && (
<span
onClick={(e) => {
e.stopPropagation();
toggleExpand(item);
}}
>
<i className={item.expand ? 'tree-expand' : 'tree-close'} />
</span>
)}
<span>{item.label}</span>
</div>
);
})}
</div>
</div>
);
}
export default VirtualTree;
|