浏览器渲染成本与 DOM 节点数直接相关:一万条数据全部生成 DOM,光创建与布局就要几秒,滚动肯定卡。虚拟列表的思路是只保留视口内可见的那一小撮节点(外加少量缓冲),用绝对定位随着滚动位置换内容——菜单始终只有十几行,但滚动表现像完整列表。
实现要点是三个映射:滚动距离对应起始索引、每项高度计算高度数组、内容区用撑高容器产生真实滚动条。其中不定高是最难的部分:未知高度要先估算并占位,渲染后回测真实高度再修正位置,React 的 react-window 与 vue-virtual-scroller 都内置了这类处理。
// 定高虚拟列表的最小实现:感受滚动的核心机制 var DATA = []; for (var i = 0; i < 100000; i++) DATA.push('第 ' + i + ' 行'); var ITEM_H = 32, VIEW_H = 400, BUFFER = 4; var viewport = document.createElement('div'); viewport.style.cssText = 'height:' + VIEW_H + 'px;overflow:auto;position:relative'; var total = document.createElement('div'); total.style.height = DATA.length * ITEM_H + 'px'; // 撑出真实滚动条 var content = document.createElement('div'); content.style.cssText = 'position:absolute;left:0;right:0;top:0'; viewport.appendChild(total); viewport.appendChild(content); document.body.appendChild(viewport); function renderItems() { var start = Math.max(0, Math.floor(viewport.scrollTop / ITEM_H) - BUFFER); var end = Math.min(DATA.length, Math.ceil((viewport.scrollTop + VIEW_H) / ITEM_H) + BUFFER); content.style.top = start * ITEM_H + 'px'; content.innerHTML = ''; for (var i = start; i < end; i++) { var row = document.createElement('div'); row.style.cssText = 'height:' + ITEM_H + 'px;line-height:' + ITEM_H + 'px;padding:0 12px;border-bottom:1px solid #eee'; row.textContent = DATA[i]; content.appendChild(row); // 始终只渲染约 VIEW_H/ITEM_H + 2*BUFFER 行 } } viewport.addEventListener('scroll', renderItems); renderItems();