应采用事件委托+状态映射代替逐个绑定,维护内存map管理选中状态,筛选时只更新数据不操作dom,超500项启用虚拟滚动或分页。

大数据量下直接用 v-for 渲染成百上千个 <input type="checkbox">,点击全选或搜索过滤时卡顿,不是 Vue 或浏览器不行,是 DOM 和响应式计算被拖垮了。
为什么 checkbox 全选会卡住主线程
常见现象:点一下“全选”,页面冻结 2–3 秒,控制台无报错,但鼠标无法响应。根本原因不是渲染慢,而是两个叠加负担:
-
this.checkedList = this.options.map(item => item.value)这类全量遍历在万级数组上本身就要毫秒级时间 - 更关键的是,每个
el-checkbox或原生input都要响应checked变化——Vue 要触发依赖收集、diff、patch;浏览器要重绘样式、更新 layout - 如果还用了
label[for]+id组合,DOM 节点数翻倍,ID 查找和无障碍属性计算也加重开销
用事件委托 + 状态映射代替逐个绑定
不给每个 checkbox 挂 change 监听器,也不依赖 v-model 同步每个值,改用单层委托 + 内存状态映射:
- 父容器加
change事件监听:document.querySelector('.checkbox-list').addEventListener('change', handler) - 在
handler中用event.target.value和event.target.checked更新一个纯对象或Map:const checkedMap = new Map(); checkedMap.set(value, checked) - 全选/反选逻辑只操作这个
Map,不触碰任何 DOM 或响应式数据 - 需要提交时再遍历
Map.keys()生成数组,避免中间态反复序列化
筛选时别动 DOM,只更新状态再批量重绘
用户输入关键词后,不要对每个 input 执行 elem.style.display = 'none'——这会强制同步 reflow,2000 项就是 2000 次重排。
- 维护一份原始数据数组(如
allItems)和一份过滤后数组(filteredItems) - 每次筛选只运行一次
filter(),结果存为新数组,不修改原数组 - 用
documentFragment或模板字符串批量生成inputHTML 片段,最后一次性innerHTML = ...替换容器内容 - 如果必须保留已有 DOM(比如有富文本或第三方插件绑定),就用
display: none控制整组<li>容器,而不是单个input
超过 500 项就该考虑虚拟滚动或分页
纯前端筛 + 全量渲染的性能拐点很明确:Chrome 下 500 个 checkbox 就开始掉帧,安卓 WebView 里 200 个就明显卡顿。
- 虚拟滚动不是必须用第三方库——手动实现核心就三点:固定容器高度、监听
scroll、按scrollTop计算可视起始索引、用slice(start, start + visibleCount)截取数据并重绘 - 分页更简单:把
allItems拆成pages数组,每次只渲染当前页的 checkbox,搜索时也只筛当前页(适合后端已分页场景) - 注意:虚拟滚动下“全选”语义要重新定义——是选当前页,还是选全部?得加显式提示,不能让用户误以为点了就真全选了后台数据
真正卡住的往往不是 checkbox 本身,而是你让它承担了不该承担的事:既是 UI 控件,又是状态源,还要实时响应筛选。把状态管理从 DOM 上剥离,性能问题就解决了一大半。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











