本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
背景回顾
在一次技术面中,面试官提问:“有没有封装过渲染千万级数据的树组件?” 我实际上没有封装过,有点懵,类似这样的都是第三方库,大概知道点思路停顿了会儿尝试回答:“监听视口区域动态加载”,便被一句 “好了,你没做过” 打断。内心 OS: 真拽,下一家。 虽然当时有些不服气,但事后冷静下来,觉得确实有必要系统性地梳理这个问题的完整解决方案。下面就是我针对这个问题的深度复盘和知识整理。
完整回答思路
我的理解:虚拟树和虚拟列表本质其实是一样的,核心原理都是只渲染可视区域内的数据。
核心逻辑:虚拟树 ≈ 虚拟列表 + 树形结构
虚拟列表的本质:只渲染可视区域元素,通过占位容器模拟滚动条。
树的特殊性:需处理层级关系、展开折叠动态变化,二者结合即为 虚拟树(Virtual Tree) 。
实现四步法:
1. 数据结构转化(TreeToList)
递归遍历树节点,转化为线性数组并记录层级、展开状态、父子关系:
function flattenTree(root, level = 0, result = []) { const node = { ...root, level, expanded: false }; result.push(node); if (node.children && node.expanded) { node.children.forEach(child => flattenTree(child, level + 1, result)); } return result;}2. 监听滚动事件
通过容器scrollTop动态计算当前可视区域索引:
const startIdx = Math.floor(scrollTop / itemHeight);const endIdx = startIdx + Math.ceil(containerHeight / itemHeight);3. 动态渲染可视节点
仅对visibleNodes = flatData.slice(startIdx, endIdx)执行 DOM 渲染。
4. 占位元素模拟滚动条
设置占位块高度为总高度 = 节点数 × 单节点高度,欺骗浏览器滚动条。
关键问题与解决策略
| 难点 | 原因 | 解决方案 |
|---|---|---|
| 展开折叠导致高度突变 | visible状态 ② 重算总高度并重置scrollTop | |
| 动态节点高度兼容 | ResizeObserver监听高度变化 ② 缓存节点实际高度,滚动用高度累加值计算 | |
| 搜索 / 定位性能瓶颈 | id -> { node, parent }) + 后端返回节点路径只展开关键分支 | |
| 内存占用暴涨 | Object.freeze冻结非活动数据 ② 使用shallowRef替代reactive | |
| 浏览器渲染上限 |
性能优化方向
1. 懒加载 + 虚拟滚动
- 初始只加载首屏数据- 展开父节点时异步请求子数据,动态插入扁平列表- 已加载节点纳入虚拟滚动管理2. 渲染性能极限优化
减少重复渲染:
v-once(Vue)或React.memo缓存静态节点GPU 加速滚动:
transform: translateY()取代top定位请求空闲期处理:用
requestIdleCallback预计算展开路径
3. 现成轮子方案
vue-virt-tree | ||
react-windowreact-tree | ||
<a-tree> |
总结:理论完备性 > 是否造过轮子
虽然未实际封装千万级 Tree,但可以明确:
本质相通:虚拟列表 → 虚拟树 实践开发原则:成熟库 + 定制化改造 > 重复造轮子 (实际开发中使用现成方案更具性价比) 抗打断话术(回答思路) :总分总,思路和表达比回答全和回答对更有意义。
下次若再遇此类问题,我会微笑反问:“贵司的 Tree 组件是自己封装,还是用 Ant Design 呢?” —— 把问题抛回去,反客为主。
作者:Neon1234