面试题:你做过的最复杂的组件是什么?

使用简单口语来回答,因为简单口语容易记忆,同时要显示出这个组件最复杂、我解决这个最复杂的组件是很不容易的。

“What is the most complex component you’ve built?”


English (natural spoken answer)

The most complex component I’ve worked on is a large-scale Virtualized Tree component, something like a file explorer used in a dashboard system.

At first glance, it looks simple, but it actually combines several hard problems together.

The main challenges were two things:

First, it’s a tree structure, not a flat list. So we need to handle nested nodes, expand and collapse states, and maintain hierarchy.

Second, we also need virtual scrolling, because the tree can contain tens of thousands of nodes. So rendering everything would completely break performance.

The hardest part was how to combine these two things efficiently.

What I did was redesign the data model. Instead of rendering the tree directly, I flattened it into a visible linear(/ˈlɪniər/) list, where each node carries its depth information.

Then I treated it like a normal virtual list, and only rendered the visible window based on scroll position.

On top of that, I optimized expand and collapse operations so they only update a small part of the visible list instead of recomputing the entire tree.

This reduced unnecessary re-rendering significantly and made the interaction very smooth even with large datasets.

Honestly, it wasn’t a trivial(/ˈtrɪviəl/ 琐碎的,容易解决的) component. It forced me to think deeply about data structure design, performance trade-offs, and rendering strategy, not just React implementation.

中文

是的,我做过最复杂的组件,是一个大规模虚拟化 Tree 组件,类似文件管理器的左侧目录结构。

一开始看起来不复杂,但实际上它同时包含几个难点:

第一,它是一个树结构,不是普通 list,需要处理层级关系、展开/收起状态。

第二,它还要支持虚拟滚动,因为节点数量可能非常大,如果全部渲染会直接卡死。

最难的地方在于:如何把 Tree 和 Virtual List 合在一起。

我当时的做法是重新设计数据结构,不直接渲染 Tree,而是把它转换成一个带层级信息的线性数组(visible list)

然后用普通虚拟列表的方式,只渲染当前可见区域。

同时在 expand / collapse 时,我没有重新遍历整棵树,而是只对受影响的子树做局部更新,从而避免了 O(N) 的全量计算。

这个优化让大数据量下的交互非常流畅。

说实话,这个组件不只是 UI 问题,而是一个涉及数据结构设计 + 性能优化 + 渲染模型设计的综合问题。

 

large list 的 virtualize我会做,但如果是large tree的 virtualize呢?可以做吗?我个人感觉就是不好确定是哪一些 nodes 显示出来,是这样吗?

你的直觉是对的,但真正的难点不是 Virtualization 本身,而是:

Tree 不是连续的数据,而 Virtual List 要求是连续的数据。

所以 Tree Virtualization = Tree Flattening + List Virtualization

先看看为什么 List 很简单

假设一个 list

Virtual List 很容易。因为

例如

index 是天然存在的。

 

Tree 为什么难?

Tree 是这种结构:

问题来了:假设

真正应该显示的是

而不是

因为它们不可见。所以:Virtual Scroll 根本不知道第 50 个 node 是谁。

 

真正的第一步:Flatten

几乎所有 Tree Virtualization 都会先做

例如:Tree

变成

注意:这里只放 visible node

 

Flatten 怎么写?

DFS (depth-First Search 深度优先搜索)即可。例如

最后得到

这个数组就是 Virtual List 的数据源。

这里的expandedSet是什么?

就是当前展开节点的 id。

 

为什么需要它?

expandedSet 用来记录 Tree 当前的展开状态,因为原始 Tree 数据只描述“结构”,不描述“用户当前看到了什么”。如果一个节点有children,但是它并没有展开,那么它的children节点就不需要计算进入 visibleNodes 里面去。

 

刚开始它是什么样的?

刚开始它可能只包含root节点,这样第一层的子节点可以看到;或者是特定的节点。反正 expandedSet 不用担心初始值,可以自定义。

 

整个流程如下:

 

result里面直接加入了node节点,如果树结构的node节点很大、很深,那么result也会相应的很大,对性能会有影响吗?


实际上不会。因为 JavaScript 的展开运算符(Spread)是浅拷贝(Shallow Copy)。只是复制了一个引用。

但是生产环境还是只将必要的属性复制过来即可,不复制整个node。

然后就和普通 Virtual List 一模一样

假设

Virtual List 根本不知道这是 Tree。它只知道

例如

直接

就结束了。

 

Expand / Collapse 怎么办?

这是第二个难点。假设

点击展开 A

以前

展开后

其实就是:重新 flatten。


很多人以为要:

实际上没必要。现代框架:

已经足够快。

 

真正的大数据怎么办?

假设

每次 DFS?那就炸了。所以真正的大型 Tree(例如 IDE、文件管理器)不会每次重新 DFS 全树。

通常会维护:

例如

点击

直接

而不是重新 DFS。

例如

Collapse

就是

这样复杂度从

变成

其中

 

最大的坑:Variable Height(这个不用管,因为我做的大概率 item height是一致的)

真正困难的其实不是 Tree。而是:

例如

Virtual List 需要知道:

如果高度不同:

就不能再简单计算:

需要维护:

很多虚拟列表库(例如基于动态尺寸的实现)都会维护每个节点的测量高度和累计偏移,滚动定位时通过二分查找找到当前可见项。

 

面试官真正想听什么?

如果面试官问:

Tree 能不能做 Virtualization?

比较完整的回答可以是:

可以。核心思路不是直接虚拟化 Tree,而是先根据当前展开状态把 Tree 扁平化(Flatten)成一个只包含可见节点的线性数组,每个节点记录自己的 depth 用于缩进显示。然后 Virtual List 就可以像处理普通数组一样,根据滚动位置只渲染可见区间的节点。

如果数据规模不大,每次展开或收起时重新 DFS 生成扁平数组即可;如果节点数量非常大,为了避免每次都遍历整棵树,通常会维护一个可见节点列表,在展开时插入对应子树、收起时删除对应后代,将更新范围限制在受影响的子树,从而把更新成本降低到与子树大小相关,而不是与整棵树大小相关。

 

 

能使用双语简单口语,来说明一下怎么做吗?

English (Interview Answer)

The main challenge is that a tree is not a flat structure like a list.

For a normal virtualized list, every item already has an index, so it's easy to calculate which items should be rendered based on the scroll position.

A tree is different because some nodes are expanded while others are collapsed. That means not every node is actually visible.

So the first step is to flatten the tree into a visible list. I usually perform a DFS traversal(/trəˈvərs(ə)l/ 遍历) and only include the nodes whose parents are expanded. During this process, I also record each node's depth so the UI knows how much indentation(缩进) to apply.

After that, virtualization becomes exactly the same as a normal list. The virtual list only works with the flattened array and renders the visible range.

If the dataset isn't huge, I simply regenerate the flattened list whenever the user expands or collapses a node.

For very large trees, rebuilding the whole list every time can be expensive. In that case, I'd maintain the visible list incrementally, inserting or removing only the affected subtree when a node is expanded or collapsed.

中文(对应理解)

可以,完全可以。

最大的区别是,Tree 不是一个连续的 List。

普通 List 本来就有 index,所以很容易根据 scroll position 算出应该渲染哪些 item。

但是 Tree 有展开和收起的状态,所以很多节点其实并不会显示出来。

因此第一步通常会先把 Tree Flatten 成一个 Visible List。我一般会用 DFS,只把当前可见的节点放进数组,同时记录每个节点的 depth,用来控制缩进。

得到这个数组以后,Virtualization 就和普通 List 完全一样了,只需要根据 scroll position 渲染当前可见范围即可。

如果数据量不是特别大,每次展开或收起时重新生成这个数组就可以。

如果 Tree 非常大,我会维护一个可见节点列表,只更新受影响的那部分子树,而不是重新遍历整棵树。


一句话总结(非常适合收尾)

Tree virtualization is basically "Tree Flattening + List Virtualization". Once the tree is flattened into a visible list, the virtualization logic is almost identical to a normal virtualized list.

中文:

Tree Virtualization 本质上就是:先把 Tree 扁平化成 Visible List,再使用普通的 List Virtualization。 这是很多成熟组件库(例如 IDE 文件树或大型文件管理器)采用的核心思路。

 

 

实现思路

核心思路

把 Tree 变成一个“带层级信息的线性数组”,然后当普通 List 做 Virtualization。

中文:

先“压平 Tree”,再用普通虚拟列表渲染。

 

Step 0:为什么 Tree 不能直接 virtualize?

Tree 是:

但是 Virtual List 需要索引:

那么因为 Tree:

所以:

❌ 不能直接 virtualize Tree

 

Step 1:关键转折(v3 的核心思想)

我们不再直接用 Tree。我们先问一个问题:

“当前页面真正显示了哪些节点?”

例如:

👉 UI 实际是:

 

Step 2:核心突破 —— Flatten Tree

我们把 Tree 转换成:

每个节点带一个关键字段:

示例结果:

 

Step 3:Flatten 怎么做?

核心就是 DFS,但有一个关键规则:

只遍历“展开的节点”

逻辑:

 

Step 4:Flatten 的结果是什么?

它不是 Tree:

而是:

 

Step 5:Virtualization 变得极其简单

现在:

我们直接:

然后:

 

Step 6:v3 的整体结构

 

Step 7:Expand / Collapse 怎么处理?

Expand:

Collapse:

注意:

❗ 每次变更都会重新 flatten

 

优缺点(面试重点)

👍 优点:

👎 缺点:

 

 

代码实现

目录结构:

 

Step1:types.ts

 

Step2:data.ts

 

Step3:flatten.ts

这是整个 Tree Virtualization 最重要的一步。

 

Step4:VirtualTree.tsx

这个组件里面包含:

 

 

Step5:App.tsx

 

运行效果

第一次:

点击 components

点击 pages

整个过程中,真正渲染的只有:

这就是 Virtualization。

 

这只是第一版(Fixed Height)

真正生产环境的 Virtual Tree(例如 VS Code Explorer、Chrome DevTools、IDE 文件树)还会继续优化,例如:

这些才是真正接近 react-arborist、VS Code Explorer 等专业 Tree Virtualization 实现的核心技术。如果你准备高级前端或 Staff 级面试,我也可以带你把这个 Demo 一步步演进成接近生产级的实现。

 

 

 

 

 

继续优化

如果让我设计一套面试学习路线,我会按下面的版本逐步演进,每一步都对应一个真实工程问题。

 

v2 Overscan(真实项目第一步)

现在我们的代码有一个问题。

假设:

滚动 1px

以前:

再滚

React 一直在:

所以滚动会有一点点抖。

真正 Virtual List 都会这样:

也就是说:

这就是 Overscan。

代码改动非常小。

以前:

改成:

是不是一下就流畅很多。

startIndex有点难理解。

仔细看,startIndex其实变小了,startIndex 变小,并不是为了"向上缓存",而是为了"用户已经滚过去但马上可能又滚回来"。

最小也就是0。

如果startIndex不变小一点,那么用户向上滚动的时候,可能就会出现空白。

 

v3 真正的大坑:Variable Height

不用管。

 

v4 真正的大型 Tree

这是 Google、VS Code 最喜欢问的。我们现在:

点击:

会:

哪怕:

也重新 DFS。

复杂度:

这就是 Demo。

真正生产不会这样。


生产环境会维护:

例如:

点击:

不会重新 DFS。

而是:

然后:

变成:

是不是只改了一小段。

复杂度:

其中:

不是:

 

v5 懒加载(Async Tree)

例如 GitHub:

根本不知道:

点击:

才:

所以 Tree 会变成:

不是:

展开:

回来:

重新插进去。

 

v6 真正生产版本

这时候你的组件其实已经接近:

架构会变成:

这里你会发现,Virtual List 已经不是重点了

真正复杂的是:

所有状态都在里面。

React Component:

几乎只是:

 

如果我是面试官,我最希望你能写出来的是哪一版?

我不会让候选人写 TanStack Virtual 那种几千行源码。

我会希望看到下面这个演进过程:

v4 是最有价值的。

因为它体现的是数据结构和算法能力,而不是会不会用虚拟列表库。

 

我建议我们下一步实现 v4

不要继续优化滚动,而是重构数据结构

我们会把现在每次都执行:

改造成维护一个长期存在的 visibleNodes 数组:

展开 components 时,我们只在数组中插入对应的子节点;收起时,只删除它的后代,而不再重新 DFS 整棵树

这一版是真正生产级 Tree Virtualization 的核心,也是很多高级前端面试最希望听到的实现思路。

 

 

 

 

 

v4 思路

太好了,我们开始做 v4:Incremental Visible List

这一版开始,我们的思路会发生一个很大的变化。

不再每次都 flattenTree(),而是把 visibleNodes 当作真正的数据源来维护。

这也是 VS Code、Chrome DevTools、很多 Tree 组件的核心思想。

 

先看看 v1 的问题

我们之前的代码:

假设 Tree 是这样的:

第一次:

点击:

React:

得到

如果 Tree 有:

每点一次:

是不是很浪费?

 

真正生产环境

生产环境不会这样。

它会一直维护:

以后:只修改这个数组。

所以:

变成:

两个同时存在。

 

第一步:VisibleNode 要记录更多信息

以前:

现在不够。因为:点击

我要知道:

所以:

其实最简单就是:

这个我们之前已经这么做了。

 

第二步:第一次还是 DFS

注意:第一次还是要 Flatten。

例如:

展开。得到:

以后:再也不用 DFS 整棵 Tree。

 

第三步:Expand

这是最重要的。

假设现在:

点击:

应该变成:

注意:

Button

Modal

是插进去的。

不是重新生成。


所以找到:

index:

例如:


然后DFS:

但是:只 DFS components。

得到:

注意:不是 DFS 整棵 Tree。

只是:


然后:

是不是完成了?

 

Collapse 怎么办?

现在:

点击:

应该删掉:

怎么删?

很多人第一反应:DFS。

其实不用。

因为数组已经是:

注意:Button Modal 都有

而pages是:

所以一直删到:

停止。


例如:

当前:

后面:

删。

继续:

删。

继续:

停止。

是不是根本不用 DFS?


所以 Collapse 非常漂亮。

下面代码的目的,就是得到应该删除的个数 removeCount。

复杂度:

k:

就是:

两项。

不是:

 

整个 Toggle 流程

以后 Expand:

Collapse:

 

下一步(真正开始写代码)

这一节我们主要理解了算法。

下一节我建议直接重构我们的 VirtualTree.tsx,把:

彻底删掉。

改成维护:

然后实现两个真正的生产级函数:

你会看到整个组件从“Demo 思路”升级为“生产级架构”,这也是很多成熟 Tree 组件内部采用的核心实现方式。

 

 

 

 

v4 代码实现

直接把 VirtualTree.tsx 改造成生产环境更接近的架构。

第一步:以前的数据流

我们以前是:

也就是说,每次点击都会重新:

 

现在的数据流

改成:

以后:render 永远使用 visibleNodes。Tree 只是数据仓库(Source of Truth)。

 

第二步:State 改掉

以前:

现在改成:

以后:render 全部来自

而不是

 

第三步:Expand

我们写一个函数:

第一件事:找到点击的是谁。

例如:

点击:

得到:

 

第四步:找到 children

例如:

就是:

 

第五步:只 Flatten 当前子树

注意!

不是:

而是:

这里还有一个小细节。

因为 Button 应该:

所以以前:

固定:

现在需要改一下。


我们修改 flattenTree() 函数,增加 baseDepth

DFS 从 depth=2 开始。


所以 flatten.ts 变成:

DFS:

 

第六步:插进去

得到:

直接:

是不是结束了?

整个过程:

没有重新 DFS 整棵树。

 

Collapse

Collapse 更漂亮。

现在:

点击:

当前:

开始:

循环:

继续。

继续。

停。

所以:

这里:

然后:

完成。

 

Toggle

最后:

 

现在复杂度是多少?

假设:

整个 Tree:

点击:

里面:

以前:

现在:

因为只处理:

是不是一下:

 

这里还有一个隐藏 Bug(也是很多面试官喜欢追问的)

来看下面这个 Tree:

假设 Button 已经展开。

也就是说:

收起components文件夹之后,再次展开:

应该出现:

而不是:

现有的代码是做不到的。

为什么?因为 Button 本来就在 expanded 集合里。

也就是说:

我们插入 components.children 时,不能只插入第一层,而要递归展开那些已经处于 expanded 状态的子节点。

这也是为什么我前面坚持保留 flattenTree(children, expanded, baseDepth) 的原因。

它不仅能生成第一层,还能自动把那些“已经展开”的子树一起展开。

注意:这里假设的是 Button 文件夹曾经展开过,如果Button文件夹本来就没有展开过,那么展开 components 文件夹,默认是不展开里面的子文件夹的。

当然,具体效果还是要看产品的需求。

 

下一节(真正生产级)

到这里,我们已经实现了:

下一步我建议继续升级,实现 NodeMap + ParentMap

 

 

下一步(了解即可)

好,下面这一步开始,就真的进入生产级 Tree 组件了。

其实我们刚刚的版本还有一个问题。

为什么还不够?

现在我们的代码里,每次点击都会:

复杂度:

如果:

每点击一次:

其实还是有点浪费。

 

真正生产环境

真正的 Tree 一般都会维护一个 Store。

例如:

React Component 只是:

你会发现:React 根本不关心 Tree。Tree 全部在 Store 里面。

treeStore里面的nodeMap、parentMap、childrenMap会在初始化时一次性构建完成,虽然刚开始会花一点时间,但是后续操作会很快,还是很值得的。

如果 Tree 只有几千到几万个节点,是没有问题的。但是,如果 Tree 的规模达到几十万甚至上百万个节点,比如 IDE 的文件系统、Git 仓库或者日志目录,那么就不会一次性初始化全部数据了。

像 VS Code 就采用了 Lazy Loading(懒加载) 的方式。刚进入页面时,只加载根节点;当用户展开某个文件夹时,再向后端请求它的子节点,同时把这些新节点加入 nodeMapparentMap 等数据结构,而不是一次性建立整个 Tree 的索引。

 

第一张 Map:nodeMap

为什么需要?

Tree:

以前想找到:

需要 DFS 整棵 Tree。

生产不会。

初始化一次:

以后:

直接:

是不是比 DFS 快很多。

 

第二张 Map:parentMap

为什么?

例如:

我要知道:

以前 DFS。

现在:

直接:

得到:

很多功能都会用。

例如:

 

初始化一次

整个 Tree 只 DFS 一次。

DFS:

以后 Tree 再也不用 DFS。

 

Expand

以前:

现在直接:

是不是:

 

Collapse

Collapse 还是扫描:

为什么?

因为 Visible List 就是:

例如:

Button 后面一定还是 Modal 。不会跑到 src 前面。

所以扫描:

就是最快。

 

面试加分点(很多人答不到)

如果面试官问:

为什么还要保留原始 Tree?为什么不只有 visibleNodes?

这是一个非常好的问题。

你的回答可以是:

English

The original tree is the source of truth. The visible list is only a projection of the tree based on the current expansion state. If I only keep the visible list, collapsing and re-expanding a node becomes difficult because I'd lose the hidden subtree structure. So in production, I keep both: the original tree stores the complete hierarchy, while the visible list is optimized for rendering.

中文

原始 Tree 是数据源(Source of Truth),而 visibleNodes 只是根据当前展开状态计算出来的一个视图。如果只保留 visibleNodes,收起节点后就丢失了被隐藏子树的层级关系,之后再展开会很麻烦。因此生产环境通常会同时保留完整 Tree 和 visibleNodes:前者负责保存完整结构,后者负责高效渲染。

 

v4 版本最终代码

好,这里给你一份真正完整的 v4(生产思路版)Virtual Tree

这一版的核心是:

❗ 不再 DFS whole tree ❗ 不再 flatten whole tree ❗ 只维护 visibleNodes + maps ❗ expand / collapse 是局部更新(O(k))

 

目录结构

 

1️⃣ types.ts

 

2️⃣ data.ts

 

3️⃣ treeStore.ts(⭐核心)

这一层就是“VS Code 思路”。

 

4️⃣ VirtualTree.tsx(Virtualization + UI)

 

5️⃣ App.tsx

 

这一版你已经实现了什么?

你现在这个 Tree 已经具备:

✅ 生产级结构思想


✅ 性能优化


✅ Virtualization

面试总结一句话(非常重要)

你可以这样收尾:

This implementation separates the tree into a data layer (TreeStore) and a view layer (visibleNodes). Instead of recomputing the whole tree on every interaction, we incrementally update the visible list in O(k) based on expand/collapse operations, and then apply standard list virtualization on top of it.

 

如果你下一步还想继续升级(建议)

我可以再带你做这三个“真正高级版本”:

🔥 v5(真实 IDE 级)

🔥 v6(超大规模优化)

🔥 v7(Figma / VSCode级)