十万级列表的跨页多选方案:el-table 踩坑与治理实践

十万级列表的跨页多选方案:el-table 踩坑与治理实践

技术洞见 :在 Element UI 表格里,UI 状态与业务状态一旦分离,selection-change 就不再只是「用户点了勾选框」------它也可能是数据刷新、分页切换、程序回显的副作用。区分这两类事件,是跨页多选能否稳定运行的分水岭。


目录

  • [1. 场景背景](#1. 场景背景 "#1-%E5%9C%BA%E6%99%AF%E8%83%8C%E6%99%AF")
  • [2. 方案概览](#2. 方案概览 "#2-%E6%96%B9%E6%A1%88%E6%A6%82%E8%A7%88")
  • [3. 踩坑与治理](#3. 踩坑与治理 "#3-%E8%B8%A9%E5%9D%91%E4%B8%8E%E6%B2%BB%E7%90%86")
  • [4. 效果与复用](#4. 效果与复用 "#4-%E6%95%88%E6%9E%9C%E4%B8%8E%E5%A4%8D%E7%94%A8")

1. 场景背景

弹窗内需要从十万级选项列表中勾选若干条目,支持搜索、前端分页、跨页保留勾选,最终提交 id 数组。

初始形态的问题

第一版只有「全部」列表,没有「已选」视图。已勾选项分散在多页中,复核时只能在全量列表里逐页翻找,找一项已选项的成本很高

为什么一次加载 10 万条、不做服务端分页

已选接口只返回 id 列表 ,不附带名称等展示字段。若列表接口做服务端分页,已选 id 很可能不在当前页,表格行缺少 projectCode / projectName,只能显示占位符或空白。

因此采用:全量列表一次拉取 + 前端过滤分页 ,用完整数据做 id → 行对象的映射;已选视图通过 selectedIds 投影,保证每条已选项都有可读展示。

需要解决的核心问题

问题 表现
翻页丢选 翻页、搜索后,其他页已勾选项从 selectedIds 中消失
主线程卡死 10 万条数据赋给 Vue 2 data,弹窗白屏数秒
UI 与状态不一致 表格勾选与 selectedIds 计数对不上

2. 方案概览

架构:业务状态与表格 UI 分离

不把 el-table 内部勾选状态当作唯一数据源,而是 selectedIds 为真相源,表格负责展示与回显

text 复制代码
┌─────────────────────────────────────────────────────────┐
│                    selectedIds                           │
│              (唯一业务状态,跨页持久)                     │
└──────────────────────────┬──────────────────────────────┘
                           │ applySelectedState / restoreSelection
                           ▼
┌─────────────────────────────────────────────────────────┐
│  computed: currentTableData(过滤 + 分页后的当前页数据)     │
└──────────────────────────┬──────────────────────────────┘
                           │ :data 绑定
                           ▼
┌─────────────────────────────────────────────────────────┐
│  el-table(展示层,触发 selection-change)                 │
│  row-key="id" + reserve-selection="true"                  │
└─────────────────────────────────────────────────────────┘

模块划分

模块 做法
选中状态 selectedIds: number[],独立于表格内部 selection
大列表 Object.freeze(list),避免 Vue 2 深度 Observer
分页/搜索 前端 computed + slice
双视图 views.all / views.selected 各自维护搜索词与分页
事件隔离 selectionSyncing 标志位,挡住程序触发的 selection-change

时序(Mermaid)

sequenceDiagram participant User as 用户/程序 participant Handler as handleSelectionChange participant State as selectedIds participant Queue as queueSelectionRestore participant Table as el-table Note over User,Table: 用户手动勾选 User->>Table: 点击 checkbox Table->>Handler: selection-change Handler->>Handler: selectionSyncing === false,放行 Handler->>State: 当前页合并进 selectedIds Note over User,Table: 翻页/搜索/切换视图 User->>State: 改分页/筛选/视图 Note over Handler: selectionSyncing = true User->>Queue: queueSelectionRestore() Queue->>Queue: $nextTick 等待 DOM 更新 Queue->>Table: restoreSelection() Table->>Table: clearSelection + toggleRowSelection Table->>Handler: selection-change(程序触发) Handler->>Handler: selectionSyncing === true,return Table->>Table: nextTick 后解锁

核心代码

跨页合并(仅用户操作时执行):

javascript 复制代码
handleSelectionChange(selectedRows) {
  if (this.selectionSyncing) return;

  const currentPageIds = this.currentTableData.map((item) => item.id);
  const selectedSet = new Set(this.selectedIds);
  currentPageIds.forEach((id) => selectedSet.delete(id));
  selectedRows.forEach((row) => {
    const id = toId(row);
    if (id !== null) selectedSet.add(id);
  });
  this.applySelectedState(Array.from(selectedSet));
}

程序回显

javascript 复制代码
restoreSelection() {
  const table = this.$refs.table;
  if (!table) {
    this.selectionSyncing = false;
    return;
  }
  this.selectionSyncing = true;
  table.clearSelection();
  const selected = new Set(this.selectedIds);
  this.currentTableData.forEach((item) => {
    if (selected.has(item.id)) {
      table.toggleRowSelection(item, true);
    }
  });
  this.$nextTick(() => {
    this.selectionSyncing = false;
  });
}

3. 踩坑与治理

坑一:selection-change 不只来自用户点击

现象

翻页或搜索后,已选数量突然变少。例如从 87 条变成 12 条。

原因

el-table 在以下情况都会触发 selection-change

  • 用户点击 checkbox
  • :data 变化(翻页、筛选、切换视图)
  • 调用 clearSelection() / toggleRowSelection()

跨页合并逻辑是「先移除当前页所有 id,再加回当前勾选行」。程序执行 clearSelection()selectedRows 为空,等价于用户取消了当前页全部勾选,其他页的 id 被误删

治理:selectionSyncing 锁机制

handleSelectionChange 入口判断标志位,程序同步期间直接 return

javascript 复制代码
// 跨页多选锁:程序化回显勾选时,el-table 仍会触发 selection-change;
// 此期间忽略该事件,避免误改 selectedIds。
selectionSyncing: false,

需要覆盖两个时间窗

时间窗 何时发生 加锁位置
窗 1:数据变更 → restore 之前 currentTableData 变了,表格重渲染 翻页/搜索/切视图入口显式 selectionSyncing = true
窗 2:restore 执行期间 clearSelection / toggleRowSelection restoreSelection 内部 selectionSyncing = true

单靠 restoreSelection 内部的锁不够:入口改完分页后,restoreSelection$nextTick 才执行,中间窗口里 data 更新触发的 selection-change 锁还没打开。

入口写法(语义清晰,推荐):

javascript 复制代码
handlePageChange(page) {
  this.selectionSyncing = true;
  this.currentView.pagination.pageIndex = page;
  this.queueSelectionRestore();
}

queueSelectionRestore() {
  // 只负责等 DOM 更新后再回显,不承担隐式加锁
  this.$nextTick(() => {
    this.restoreSelection();
  });
}

queueSelectionRestore 使用 $nextTick 的原因:分页/筛选变更后,currentTableData 需先传到 el-table 并完成渲染,再对新页数据 执行 toggleRowSelection,否则会操作到旧页 DOM。


坑二:Vue 2 大数组赋值导致主线程卡死

现象

接口返回 10 万+ 条记录,this.list = res 之后弹窗白屏 3~8s

原因

Vue 2 对 data 中的数组递归 defineProperty,10 万对象 × 多字段 ≈ 数十万次属性劫持,主线程长时间阻塞。

治理:Object.freeze

javascript 复制代码
// 冻结后 Vue 2 不建立深度 Observer,避免 defineProperty 风暴
this.list = Object.freeze(normalizeList(res));

注意:

  • 冻结后不能原地修改数组元素,需整体替换
  • 整体赋值仍会触发依赖 listcomputed 重算,过滤/分页能力不受影响
  • 搜索关键字在外层 normalizeKeyword 一次处理,避免 filter 内对每条记录重复 trim/toLowerCase

效果 :本地 Chrome 实测,弹窗可交互时间从 ~6s 降至 ~1.2s


坑三:reserve-selection 不能替代业务层状态

现象

开了 row-key + reserve-selection,翻页后勾选仍偶发丢失;或「已选」视图与「全部」视图切换后状态混乱。

原因

  1. reserve-selection 维护的是表格内部 selection store,不是业务层的 selectedIds
  2. :data 整体替换时,内部状态可能重置,业务层无感知
  3. 「已选」视图的 data 是全量列表的子集投影,与 reserve-selection 假设的「同一数据源翻页」场景不同

治理

用独立 selectedIds 做真相源,restoreSelection 在每次 data 变化后主动回显。reserve-selection 仅作辅助,不作为状态来源。


坑四:只有「全部」视图时难以复核已选项

现象

已选 50 条分散在 10+ 页,保存前要在全量列表里逐页核对,极易遗漏。

治理

增加「已选」视图:selectedIds 投影为 selectedList,独立搜索与分页。取消勾选后列表即时收缩,复核成本从逐页翻找降为单列表浏览。

双视图需隔离状态------views.allviews.selected 各维护 searchTextsearchKeywordpagination,避免共用一个列表硬切 filter 导致搜索词、页码互相污染。


坑五:加锁位置放错导致偶发丢选

现象(开发期)

仅在 restoreSelection 内加锁,翻页时偶现已选数量减少。

根因

时序如下:

text 复制代码
handlePageChange
  → pageIndex 同步变更,currentTableData 立即变化
  → el-table 因 data 更新触发 selection-change(此时锁未开)
  → $nextTick 后才执行 restoreSelection(锁才生效)

治理

改变表格数据源之前 于各入口显式 selectionSyncing = truequeueSelectionRestore 只负责 $nextTick 调度,不隐式承担加锁副作用------否则读代码时不易发现「这里会忽略 selection-change」。


4. 效果与复用

优化效果

指标 优化前 优化后
弹窗可交互(10 万条) ~6s 白屏 ~1.2s
跨页丢选 翻页/搜索可复现 0 复现
已选复核 在全量列表逐页翻找 「已选」视图集中展示

可复用模式

适用于一切「海量选项 + 弹窗多选 + 跨页保留」场景,可归纳为三件套:

  1. TruthselectedIds 为唯一业务状态
  2. LockselectionSyncing 区分程序事件与用户事件
  3. Timing$nextTick 后再 restoreSelection,对齐 DOM 与数据

Code Review 检查项

  • 改变 currentTableData 前,是否已 selectionSyncing = true
  • queueSelectionRestore 是否只负责 $nextTick 调度?
  • handleSelectionChange 入口是否有锁判断?
  • 大数组是否已 Object.freeze 或等价降 Observer 方案?
  • 已选接口只返回 id 时,是否有 id → 行对象的映射保障?

结语

跨页多选的本质,是在 UI 事件与业务状态之间划清边界selectionSyncing 只有一行判断,却挡住了 el-table 在数据刷新、程序回显时误触发的 selection-changeObject.freeze 一行代码,化解了 Vue 2 大数组的 Observer 风暴。两者都不花哨,但都是这个场景里真正管用的治理手段。


技术关键词Vue 2 · Element UI · el-table · 跨页多选 · selection-change · selectionSyncing · Object.freeze · 前端状态同步 · 大列表性能优化


本文基于真实项目经验整理,手工起草文章大纲,AI 辅助润色,于 2026-06-05

相关推荐
rising start2 小时前
二、Vue3 核心基础:API 对比、Setup 与响应式详解
前端·javascript·vue.js
我穿棉裤了3 小时前
解决el-form表单校验时显示的红色星号与文字对齐的问题
前端·javascript·vue.js
超人不会飞_Jay3 小时前
2026.6.4 Vue用户中心项目笔记
前端·vue.js·笔记
懂懂tty4 小时前
Vue3 编译优化
前端·javascript·vue.js
踩着两条虫4 小时前
VTJ.PRO v2.4.0 多人协作与 AI 批量识图实战评测
vue.js·人工智能·低代码·figma
低保和光头哪个先来4 小时前
源码篇 生命周期
前端·javascript·vue.js
ct9784 小时前
Vue 项目性能优化
前端·vue.js·性能优化
辞忧九千七4 小时前
Vue3 学习:组件通信完全指南
vue.js
LIUAWEIO18 小时前
vue里面下载配置使用zepto vue中怎样使用zepto
javascript·vue.js·es6·zepto