ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

虚拟列表:高频核心面试题 + 追问链

虚拟列表:高频核心面试题 + 追问链 1. 虚拟列表和普通列表懒加载的核心区别是什么面试者真正应该说出口的答案普通列表懒加载只是把 DOM 的创建过程分批进行用户滚得越远DOM 通常越积越多虚拟列表只维护可视区域和附近的一小批 DOM滚动时复用或更新这些节点所以 DOM 数量基本不会随着数据总量增长。这句话是这道题最核心的区分。面试官继续追问后的展开比如有 10 万条数据普通列表懒加载滚动 ↓ 加载下一批数据 ↓ 数据越来越多 ↓ DOM 也越来越多 ↓ 最终还是可能卡它优化的是网络请求和数据加载成本。而虚拟列表10 万条数据 ↓ 数据可以全部存在内存 ↓ 根据滚动位置计算当前应该显示哪些数据 ↓ 只创建这一小段数据对应的 DOM ↓ 滚动时不断复用/更新这部分 DOM它优化的是DOM 数量和渲染、布局、绘制相关成本。所以二者甚至可以同时使用分页 / 懒加载 ↓ 控制数据加载量 虚拟列表 ↓ 控制 DOM 数量2. 为什么普通列表数据一多就会卡真正的问题不是“数据多”而是大量数据最终变成了大量 DOM而 DOM 增多会带来渲染、布局、绘制以及更新成本。面试官继续追问后的展开例如ulli第 1 条/lili第 2 条/li...li第 100000 条/li/ul100000 条数据如果全部渲染就意味着浏览器需要维护大量节点。问题会逐层出现数据量大 ↓ DOM 节点多 ↓ 样式计算 / Layout / Paint 等成本增加 ↓ 首次渲染慢 ↓ 更新成本增加 ↓ 滚动、交互容易出现卡顿所以虚拟列表真正解决的是让“数据量”和“DOM 数量”解耦。这是比“减少 DOM”更重要的一层理解。3. 虚拟列表到底是怎么实现的这是第一道真正的技术分水岭。核心就是根据现在滚到的位置算出这一屏要显示哪些数据只渲染这一小部分 DOM剩下的用空白区域把滚动条撑起来。面试官继续追问后的展开假设总数据10000 条 每一项高度50px那么整个列表理论高度10000 × 50 500000px但实际上屏幕可能只能显示20 条左右所以页面不需要真的创建 10000 个 DOM。可以变成┌─────────────────────┐ │ 空白占位 │ ← 上方 spacer ├─────────────────────┤ │ item 101 │ │ item 102 │ │ item 103 │ │ ... │ ← 真正渲染 │ item 120 │ ├─────────────────────┤ │ 空白占位 │ ← 下方 spacer └─────────────────────┘核心就是三个东西总高度 当前可视范围 可视范围对应的数据4. 固定高度虚拟列表怎么计算这是非常高频的追问。假设itemHeight 50px scrollTop 5000px那么当前滚动到了5000 / 50 100也就是说startIndex ≈ floor(scrollTop / itemHeight)假设 viewport 高度是500px那么可视数量大约500 / 50 10实际工程一般不会只渲染这 10 个而是增加一个 bufferstartIndex ↓ 前面多渲染几个 [buffer] [可视区域] [buffer]例如scrollTop ↓ ────────────┬──────────── │ item 100 item 101 item 102 ... item 120 │ ────────────┴────────────最终得到conststartIndexMath.floor(scrollTop/itemHeight);constvisibleCountMath.ceil(viewportHeight/itemHeight);constendIndexstartIndexvisibleCountbuffer;然后constvisibleItemslist.slice(startIndex,endIndex);这就是最基础的固定高度虚拟列表。5. 为什么还需要一个“占位容器”因为实际只渲染了一小部分 DOM需要用占位空间撑起完整列表高度保证滚动条正常。比如10000 条 × 50px 500000px我们实际只渲染20 条如果没有占位高度浏览器看到的高度 20 × 50 1000px滚动条当然就只有这么长。所以虚拟列表通常会.list{height:500000px;}然后真正的内容通过transform:translateY(...)或者top:...移动到正确的位置。所以你可以把它理解成完整列表的“空间”存在 完整列表的“DOM”不存在这句话非常适合面试。6. 滚动的时候发生了什么滚动时根据新的scrollTop重新计算 startIndex 和 endIndex然后更新这一小段 DOM 的内容和位置。完整流程用户滚动 ↓ scrollTop 变化 ↓ 计算 startIndex ↓ 计算 endIndex ↓ 得到新的可视数据 ↓ 更新 DOM ↓ 移动内容容器例如第一次 startIndex 100 endIndex 120 渲染 100 ~ 120继续往下滚startIndex 120 endIndex 140于是100 ~ 120 ↓ 120 ~ 140这里还有一个非常重要的工程优化不一定真的销毁 100~120再创建 120~140。优秀的虚拟列表会尽可能复用已有 DOM 更新数据 调整位置所以它进一步减少了频繁创建和销毁 DOM 的成本。7. 为什么虚拟列表滚动时不会出现明显跳动因为数据项的实际位置和滚动位置之间必须建立稳定的映射固定高度可以直接通过索引计算动态高度则需要额外维护每一项的真实高度。固定高度非常简单index × itemHeight例如第 100 项 100 × 50 5000px所以定位非常准确。8. 如果每一项高度不固定怎么办这才是更高级的追问。比如item 1 50px item 2 100px item 3 80px item 4 200px ...这时候index*itemHeight已经不能用了。因为第 100 项的位置 ≠ 100 × 一个固定高度通常需要预估高度 ↓ 先计算一个大概位置 ↓ 真正渲染 ↓ 测量真实高度 ↓ 更新高度缓存 ↓ 修正后续位置高度缓存可能类似heightMap{0:50,1:100,2:80,3:200};然后通过累计高度计算某个 index 的实际位置。数据量非常大时还会进一步使用前缀和 二分查找 Fenwick Tree / Segment Tree等数据结构优化“根据滚动位置反查 index”的成本。这就是动态高度虚拟列表比固定高度复杂很多的原因。9. 虚拟列表真正优化的是什么数据量 N ↓ 普通列表 N 条数据 → N 个 DOM ↓ 渲染 / Layout / Paint / 更新成本随 N 增长 虚拟列表 N 条数据 ↓ 只保留 O(k) 个 DOM k ≈ 可视区域 buffer ↓ DOM 数量与总数据量解耦所以它最核心的价值是把 DOM 的规模从跟数据总量相关变成跟可视区域相关。这比“减少 DOM”更接近底层。10. 虚拟列表是不是一定比普通列表快不是。这是非常容易被追问的边界问题。如果只有20 条数据你上虚拟列表反而可能增加复杂度scroll 监听 索引计算 DOM 复用 位置计算 边界处理所以虚拟列表不是“任何列表都应该使用。”而是当列表数据量足够大普通渲染已经成为瓶颈时虚拟列表才有明显收益。11. 虚拟列表的主要矛盾是什么主要矛盾数据量很大但用户同一时刻真正能看到的数据很少。普通列表100000 条数据 ↓ 100000 个 DOM虚拟列表100000 条数据 ↓ 用户只看到几十条 ↓ 只维护几十到几百个 DOM所以它实际上是在解决数据规模和可视 DOM 规模不匹配的问题。12. 虚拟列表有哪些工程边界高级面试可以继续追问① 动态高度最麻烦的问题之一真实高度未知 → 位置不好计算 → 高度变化可能导致滚动位置修正② 快速滚动用户快速拖动滚动条scrollTop 100 500 2000 10000 50000如果每次 scroll 都同步计算和更新 DOM可能造成新的性能问题。通常需要节流 requestAnimationFrame buffer DOM 复用③ 大量复杂子组件虚拟列表只能减少DOM 数量如果每个 item 本身就非常复杂Item ├── React/Vue 组件 ├── 图片 ├── 计算 ├── 状态 └── 事件仍然可能卡。④ 图片加载虚拟列表不能自动解决图片请求 图片解码 图片绘制所以通常还需要图片懒加载 尺寸预留 缓存⑤ 无障碍和搜索虚拟列表只存在部分 DOM因此键盘导航 焦点管理 浏览器查找 无障碍阅读器都需要额外处理。13. 最后一道如果让你自己实现一个虚拟列表你怎么设计这道题非常适合拉开 3 年和 5 年 前端的差距。我会先确定列表是固定高度还是动态高度固定高度直接通过scrollTop和itemHeight算可视索引动态高度则需要做高度缓存和位置修正然后通过上下占位空间保持完整滚动高度实际只维护可视区域加 buffer 的 DOM。然后展开Virtual List │ ┌────────────┴────────────┐ ↓ ↓ 固定高度 动态高度 │ │ scrollTop / itemHeight height cache │ │ 直接计算 index 计算累计高度 │ │ └────────────┬────────────┘ ↓ startIndex ↓ endIndex ↓ visibleItems buffer ↓ DOM 复用 ↓ translateY 定位 ↓ spacer 撑起总高度14. 最终满分答案面试官问虚拟列表是什么为什么需要它虚拟列表就是数据很多但 DOM 不需要全部创建只根据当前滚动位置渲染可视区域附近的一小部分数据。普通列表的问题是数据量一大最终会变成大量 DOM进而增加渲染、布局、绘制和更新成本。虚拟列表把这两个东西解耦数据可以有 10 万条 ↓ 实际 DOM 只保留可视区域 buffer ↓ 滚动时根据 scrollTop 重新计算数据范围 ↓ 更新并复用这部分 DOM同时用占位空间撑起完整列表高度让浏览器的滚动条仍然对应 10 万条数据。固定高度的情况下可以直接通过startIndex ≈ scrollTop / itemHeight计算当前应该渲染哪些数据如果是动态高度就需要额外维护每一项的真实高度和位置。所以虚拟列表最核心的价值不是简单的“优化长列表”而是让 DOM 数量从跟总数据量相关变成跟可视区域相关。而普通列表懒加载解决的是“什么时候加载数据”虚拟列表解决的是“什么时候创建和保留 DOM”两者可以一起使用。这套答案已经能从“会背概念”进入到“理解底层实现”的层次。真正的大厂追问重点就落在scrollTop → index → 可视数据 → DOM 复用 → 定位 → 动态高度这条链上。
返回列表