虚拟滚动不处理多选逻辑,卡顿源于全量遍历与频繁重绘;应解耦状态管理,用Set存ID实现O(1)查找,并维护totalVisibleCount、checkedInVisible、totalSelectedCount三个派生状态优化全选/半选判断。

虚拟滚动本身不处理多选逻辑,它只负责渲染可视区域内的 DOM。多选卡顿的根源在于:每次点击都触发全量数据遍历、状态更新、UI 重绘和复杂判断(如全选/半选状态同步)。要流畅支持多选,关键不是“把多选塞进虚拟滚动”,而是让多选状态管理与虚拟滚动解耦,并用空间换时间做优化。
用 Set 或 Map 管理选中 ID,避免数组遍历
不要用 selectedItems.includes(item.id) 或 selectedItems.indexOf(item.id) !== -1 判断是否选中——随着选中项增多,每次 render 都是 O(n)。改用 Set 存 ID:
-
初始化:
const selectedIds = new Set(); -
点击切换:
selectedIds.has(id) ? selectedIds.delete(id) : selectedIds.add(id); -
渲染判断:
selectedIds.has(item.id)—— O(1) 查找
分离“全选状态”计算,不依赖实时遍历
全选按钮的状态(全选 / 半选 / 未选)不应每次点击都遍历全部数据。改为维护三个派生状态:
- totalVisibleCount:当前可视区域内总条目数(由虚拟滚动器提供)
- checkedInVisible:当前可视区域内已勾选项数(点击时增减)
- totalSelectedCount:全局已选总数(点击时增减)
这样,“全选按钮是否为全选态”只需判断 checkedInVisible === totalVisibleCount;“是否半选”看 checkedInVisible > 0 && checkedInVisible ;而“是否禁用全选”或“是否显示已选 X 条”直接读 totalSelectedCount,无需任何循环。
批量操作防抖 + 异步提交,避免高频点击压垮 UI
用户快速连点或 Ctrl+Click 多个条目时,频繁触发 React 状态更新会引发多次 re-render。解决方案:
- 对
selectedIds的变更做微任务合并(如用Promise.resolve().then(...)延迟到下一帧) - 全选/取消全选等批量动作加简单节流(例如 100ms 内只执行最后一次)
- 若需同步后端,把勾选变更暂存为操作队列,用
requestIdleCallback或setTimeout(..., 0)异步批量提交
必要时启用“懒同步”:滚动中暂停状态更新
用户快速滚动时,频繁更新 checkedInVisible 和重新计算全选态反而造成卡顿。可监听滚动中状态,在 scrolling 为 true 期间只记录操作意图(如 push 到 pending 队列),滚动停止后再批量应用:
- 监听
onScroll或使用 IntersectionObserver 检测滚动中 - 滚动中仅缓存操作:
pendingOps.push({ type: 'toggle', id }) - 滚动结束(debounced)后统一执行并刷新视图
这种策略在万级数据、百项每页的场景下,能让多选交互从明显卡顿变为几乎无感。









