Element UI 表格分页多选跨页勾选记忆与批量操作实践 1. 先把需求拆开看分页选中记忆到底难在哪接手一个后台列表页面时产品需求往往写得很短表格分页展示每行前面加个勾选框运营翻到第三页勾了几条再翻回第一页之前勾的必须还在最后点“批量导出”要能把几页勾过的数据全都导出去。看着像个一行代码的活儿但真动手写十有八九会卡在 Element UI 的el-table上——第一版代码往往长这样在el-table标签上加了:reserve-selectiontrue翻页回来发现该丢还是丢于是开始怀疑是框架的锅。问题基本都出在属性挂错了位置。row-key是el-table的属性而reserve-selection是el-table-column的属性只管typeselection那一列。这两个东西一个管“认人”一个管“记性”缺一个都白搭。这篇就把这套东西从头捋一遍为什么选中会丢、row-key到底怎么要求、翻页回填要怎么写、搜索和删除之后要怎么收场再附一份我自己的项目里反复用过的封装代码。适合正在写后台列表、被这个需求折磨过一次的中级前端也适合刚上手 Element UI、想知道“为什么这么写”的同学。1.1 后台列表里藏着的三条隐性要求表面上看需求只有一句“勾选要记住”但实际交付时验收标准通常会膨胀成三条。第一条是跨页可见从第 1 页翻到第 5 页再翻回来之前勾过的行必须还是勾着的表头全选框的半选状态也要跟着对。第二条是跨页计数页面上方通常要显示“已选 12 条”这个数字是全局的不是当前页的。第三条是跨页提交点导出或者批量操作的时候提交的是全部勾选行的主键而不是当前页的。这三条要求对应的技术手段完全不同。第一条靠el-table自己就能做一半靠的是reserve-selection加row-key第二条得自己维护一份选中集合因为el-table内部的selection数组虽然会保留跨页数据但里面存的是对象引用不是干净的 id 列表第三条最容易被忽略很多人直接在selection-change里把val赋值给一个变量就去做提交结果发现提交上去的数据字段是旧的、少了几个新增字段、甚至有几条是因为数据被后台改过导致状态不一致。三条要求层层递进只满足第一条就以为做完了上线之后必然被返工。1.2 el-table 为什么会把选中状态弄丢要理解这个得先知道el-table是怎么判断“某一行被选中了”的。它内部维护一个selection数组每次渲染对每一行都会问一句这一行在selection里吗如果在就画上勾。判断“在不在”的方式取决于你有没有配row-key。没配row-key的情况判断依据是对象的引用地址也就是selection.indexOf(row) -1。这就出问题了分页切换时父组件把list整个换成了新的一页数据新数据是接口返回的新对象地址跟selection里存的老对象完全对不上于是所有勾都掉了。更糟的是就算你翻回原来那一页接口重新请求了一次返回的又是一批新对象还是对不上。这就是为什么很多人觉得“我没改代码啊怎么翻页就没了”。配了row-key的情况判断依据变成row[rowKey] selection[i][rowKey]也就是按业务主键比对。只要两次拿到的行 id 一样就能认出是同一条数据勾选状态自然能延续。但这里还有个前提el-table默认在data变化的时候会把selection里那些“当前data里找不到”的行清掉。reserve-selection的作用就是关掉这个清理动作让不在当前页的选中项继续躺在selection数组里。所以这两个属性是配合关系不是二选一。1.3 两条路线的取舍组件内保留还是自己维护集合真要做这个功能其实有两条路。第一条是借力路线靠row-keyreserve-selection让el-table内部帮我们维护跨页选中第二条是自管路线自己维护一个Set或者数组存 id在selection-change里同步在翻页、搜索、增删之后用toggleRowSelection手动回填。两条路的差别挺明显的我列个表对比一下维度借力路线reserve-selection自管路线自己维护 id 集合代码量两个属性搞定约 5 行需要写回填、同步、清理逻辑约 60 行起数据新鲜度selection里是历史快照字段可能过期只存 id提交前用最新数据合并搜索过滤过滤掉的选中项仍留在 selection 里容易计错数想清就清想留就留完全可控跨路由/刷新记忆做不到组件销毁即丢存 sessionStorage 就能带过去树形表格父子联动跨页时状态容易错乱自己算逻辑清楚适用场景简单列表、只有翻页没有搜索复杂筛选、批量操作、多 Tab 复用我自己的选择标准是这样如果这个表格只有分页、没有搜索框、没有行删除、选完立刻提交就直接用借力路线五分钟完事。但凡上面有一条不满足就直接上自管路线别贪那几行代码后面补丁会补到你怀疑人生。真实项目里我基本都走自管路线因为“只有分页没有筛选”的表格几乎不存在产品经理迟早会给你加个搜索框。2. 原理拆解row-key 和 reserve-selection 各管什么2.1 选中状态到底存在哪儿很多人写这个功能时脑子里没有状态归属的概念导致到处找“为什么”。实际上el-table的选中状态有两处一处是组件内部 store 里的selection数组另一处是父组件通过selection-change事件拿到的那份数据。这两份数据是引用关系父组件拿到的数组就是内部selection的映射你改它等于改内部状态虽然不推荐这么干。翻页的时候data变化会触发内部的一轮“对账”遍历selection看看每一项还能不能在当前data里找到。找到就留着找不到就看reserve-selection。为false默认时直接剔除为true时保留。所以从行为上描述reserve-selection做的事情非常单纯在数据更新时不要把不在当前页的选中项删掉。理解这一点之后很多“怪现象”就有解释了。比如你在第二页勾了三条翻到第三页selection-change不会触发因为selection数组本身没变。比如你翻回第二页那三条会重新显示勾选因为内部按row-key比对上了。比如你在第一页点全选只会选中第一页的十条selection变成十条不会变成“全部数据”。这些行为都是同一套逻辑的推论不是 bug。2.2 row-key 的三个硬性要求row-key看起来就是个属性随便填填也能跑但出问题的时候九成出在它身上。我总结下来有三条硬要求。第一必须唯一。后端返回的id是首选。但如果数据里混了id为null或者undefined的行el-table会把它们当成同一行——多个null相互匹配勾一个全勾。这种情况在临时新增行、草稿行里很常见处理办法是给没有 id 的行分配一个本地临时 key比如temp_${Date.now()}_${index}。第二必须稳定。同一行数据在两次请求里row-key的值必须一样不能这次是数字1001下次变成字符串1001。el-table内部是用全等比较的数字和字符串永远不等表现就是“明明 id 一样怎么不勾选”。这个问题在对接某些后端时特别容易踩——分页接口返回数字详情接口返回字符串或者JSON.parse大整数时精度丢失导致 id 变了。第三尽量用字符串。对象 key 会被隐式转成字符串用数字做 key 时虽然大部分场景正常但遇到精度问题就会翻车我一般会在模板里写:row-keyrow String(row.id)一劳永逸。还有一个细节值得提row-key写函数的时候每次渲染都会调用行数上千的话会有性能影响。我实测过 800 行数据、每行row-key是函数调用的表格滚动时会有一点卡改成字符串属性名row-keyid就顺畅了。数据量大的话别用函数。2.3 selection 里存的是对象引用不是你以为的 id这是最容易埋雷的一点。开了reserve-selection之后selection数组里保留的跨页项是第一次被选中时那个 row 对象的引用不是新数据。也就是说你在第二页勾了一行然后翻到第五页后台把这行的状态从“待审核”改成了“已通过”你再回到第二页界面上显示的是“已通过”因为重新请求了数据但selection数组里躺着的那个对象status还是“待审核”。带来的直接后果是如果你在提交时直接用this.multipleSelection.map(row row.status)拿到的会是过期数据。真实项目里出过更隐蔽的问题——批量操作时需要提交“操作前的版本号”做乐观锁校验结果提交上去的是上一次选择时的旧版本号后端直接判定冲突用户看到的是“部分数据操作失败”排查了半天才定位到是选中项快照的问题。解法很简单永远不要直接拿selection里的对象去做业务提交只从中提取主键。提交前用主键去和最新的完整数据做合并或者干脆只提交 id 数组让后端自己查。这既是数据正确性的要求也是减少请求体积的实际考虑。我在封装的时候会强制把selection转换成selectedIds数组再往上传父组件根本拿不到那份对象引用从源头上把这个坑堵死。3. 动手实现一份能直接抄的多选表格3.1 最小改动版本注意属性写在哪一层先给一个最小可用的版本适合“只有分页、没有搜索”的简单场景。模板部分的关键是三处el-table上写row-keyel-table-column上写reserve-selection然后监听selection-change。template div el-table reftable :datalist row-keyid selection-changehandleSelectionChange el-table-column typeselection width48 :reserve-selectiontrue / el-table-column propname label名称 / el-table-column propstatus label状态 / /el-table el-pagination :current-pagepage :page-sizesize :totaltotal layoutprev, pager, next, total current-changehandlePageChange / div已选 {{ selectedIds.length }} 条/div /div /template这里最值得强调的是reserve-selection的位置。我见过至少四五次有人把它写在el-table上然后跑来问为什么没效果。Element UI 的文档里这个属性确实归属于el-table-column它的语义是“这一列选择列在数据更新后保留之前选中的记录”写在表上语义上说不通代码自然也不认。同样容易搞混的还有:selectable那是控制某一行能不能被勾选的也挂在el-table-column上。还有一个隐含条件选择列一般要放第一列。虽然技术上放后面也能跑但reserve-selection在固定列和动态列的场景下偶发状态不同步我习惯性放第一列避开了这类边缘情况。另外选择列不要写成v-if动态渲染切换显示时selection会被重置跨页记忆直接失效。3.2 服务端分页下怎么把勾选状态画回界面上如果是前端一次性拿全量数据自己分页el-table只渲染当前页reserve-selection基本够用。但绝大多数后台是服务端分页每次翻页都是一个新请求返回一页新数据。这时候reserve-selection依然有效因为它比的是row-key而不是数据引用翻回来时主键能对上勾选状态就能画出来。但有一个前提必须满足新数据里必须包含完整的row-key字段。我遇到过后端为了省带宽翻页接口返回的列表数据里把id字段去掉了详情接口才给前端就只能靠index做 key这时候跨页记忆彻底失效。这种问题在前端是解不掉的得回去让后端把主键带上别自己在前端编造假 key一旦做排序、筛选就全乱。还有一种情况是数据要经过前端二次处理比如用map给每行加一个计算字段。如果处理过程中不小心把id删了或者改名了比如解构时漏了id也会导致比对失败。我的习惯是在请求回来的第一层就做一次完整性检查async fetchList() { const res await api.getList({ page: this.page, size: this.size }); this.list (res.data.list || []).map(item { if (item.id undefined || item.id null) { console.warn([table] 缺少主键跨页记忆将失效, item); } return item; }); this.total res.data.total; }这条console.warn在开发环境里救过我两次都是在联调早期就发现了字段缺失比上线之后才暴露强得多。3.3 全选和半选只能覆盖当前页的现实el-table的全选框有个明确的行为边界它只对当前data里可见的行生效。你在第一页点全选选中的是第一页那十条selection变成十条翻到第二页全选框是未勾选状态因为第二页的十条不在selection里。这个行为不是缺陷而是设计如此因为组件只能看到当前的数据。但产品经理的预期往往是“全选所有页”。这个需求需要自己实现思路是维护一个“是否全选全部”的标记配合手动toggleRowSelection。我在项目里的做法是handleSelectAll(selection) { // selection 是当前页被选中的行 const currentPageIds this.list.map(i i.id); if (selection.length 0) { // 当前页被取消全选从集合里移除当前页所有 id currentPageIds.forEach(id this.selectedMap.delete(id)); } else { currentPageIds.forEach(id this.selectedMap.set(id, true)); } this.syncSelectedIds(); }关键是不要试图让el-table自己的全选框表达“全选了所有页”的语义它做不到强行改内部状态只会让半选计算乱掉。正确做法是在表格外面单独放一个“全选全部结果”的复选框跟表头的全选框区分开文案写清楚是“选中本页”还是“选中全部匹配结果”。用户其实能理解这个区分含糊其辞才会引发困惑。半选状态也是同理el-table计算的半选是基于当前页数据的所以当你在第一页勾了 3 条、翻到第二页时第二页的表头是全不选状态而不是半选。这不是错如果产品非要“全局半选”同样得在外面自己做一个指示器。3.4 交给后端的应该是一串 id而不是 selection这是整个方案里最容易被忽略的一步。selection-change给你的val是对象数组直接丢给接口有三个问题数据可能是过期的、请求体积大、字段里可能含有敏感信息被带到日志里。正确姿势是事件里只做 id 收集业务提交时用 id。handleSelectionChange(selection) { // 不要 this.multipleSelection selection // 只提取主键并且保留一份用于展示的轻量信息 this.selectedIds selection.map(row row.id); this.selectedBrief selection.map(row ({ id: row.id, name: row.name })); this.$emit(selection-change, { ids: [...this.selectedIds], count: this.selectedIds.length }); }如果业务确实需要提交时带上完整数据比如导出时需要行内的所有字段那也别用selection里的老对象而是用 id 去当前最新数据里找找不到就单独请求一次详情。看起来多了一次请求但换来的是数据一致性批量操作出错时排查成本会低很多。金额、状态、版本号这类字段一旦带了旧值问题往往不是立刻暴露而是几天后从用户反馈里冒出来那时候查起来极其痛苦。3.5 一份可直接复用的完整代码下面这份是我自己在多个后台列表里复用过的版本去掉了业务字段保留了核心逻辑。功能包括跨页记忆、跨页计数、选中项被删除后自动清理、搜索后可选是否保留选中。template div classpage div classtoolbar el-input v-modelkeyword placeholder搜索名称 clearable clearhandleSearch / el-button typeprimary clickhandleSearch查询/el-button span classtip已选 {{ selectedIds.length }} 条/span el-button :disabled!selectedIds.length clickhandleBatch批量操作/el-button /div el-table reftable :datalist row-keyid selection-changehandleSelectionChange el-table-column typeselection width48 :reserve-selectiontrue / el-table-column propname label名称 / el-table-column propstatus label状态 / /el-table el-pagination :current-pagepage :page-sizesize :totaltotal layoutprev, pager, next, total current-changehandlePageChange / /div /template script export default { name: SelectionMemoryTable, data() { return { list: [], total: 0, page: 1, size: 10, keyword: , selectedIds: [], selectedBrief: [] }; }, methods: { async fetchList() { const res await api.getList({ page: this.page, size: this.size, keyword: this.keyword }); this.list res.data.list || []; this.total res.data.total || 0; this.$nextTick(this.restoreSelection); }, // 关键新数据渲染完之后按 row-key 把勾选状态画回去 restoreSelection() { if (!this.selectedIds.length || !this.$refs.table) return; const idSet new Set(this.selectedIds.map(String)); this.list.forEach(row { const shouldSelect idSet.has(String(row.id)); const isSelected this.$refs.table.selection.some( item String(item.id) String(row.id) ); if (shouldSelect ! isSelected) { this.$refs.table.toggleRowSelection(row, shouldSelect); } }); }, handleSelectionChange(selection) { this.selectedIds selection.map(row row.id); this.selectedBrief selection.map(row ({ id: row.id, name: row.name })); }, handlePageChange(page) { this.page page; this.fetchList(); }, handleSearch() { this.page 1; this.fetchList(); }, handleBatch() { this.$emit(batch, { ids: [...this.selectedIds] }); }, // 供父组件调用某条数据被删除后把它从选中集合里摘掉 removeSelected(id) { const idx this.selectedIds.indexOf(id); if (idx -1) this.selectedIds.splice(idx, 1); const row this.list.find(item item.id id); if (row) this.$refs.table.toggleRowSelection(row, false); }, clearAll() { this.selectedIds []; this.selectedBrief []; this.$refs.table.clearSelection(); } } }; /scriptrestoreSelection里那个“先比对再切换”的判断是有意加的。如果无脑调用toggleRowSelection(row, true)在已经选中的行上再调一次el-table不会做幂等处理selection-change会重复触发进而引发无限循环的风险尤其在selection-change里又调用了回填逻辑的写法下。加这个判断成本极低能避免很多诡异的重复触发。4. 边界场景搜索、删除、封装与持久化4.1 搜索之后之前勾的还算不算数这是所有问题里争议最大的一个也没有标准答案得看业务语义。我的判断原则是如果搜索是“缩小范围”选中项应该保留如果搜索是“切换业务集合”选中项应该清空。举个例子。用户在一个订单列表里勾了 5 单准备批量发货然后输入关键词筛出某个客户这时候他多半是想“在这个客户里再补勾两单”之前的 5 单应该保留。但如果用户切换的是“订单类型”这种一级筛选从“销售订单”切到“采购订单”那之前的选中就毫无意义应该清空。实现上如果选了保留要处理一个视觉上的问题被筛掉的选中项在界面上看不见但“已选 5 条”的数字还挂着用户会疑惑。我在项目里的做法是在数字旁边加一个可展开的标签区把已选中的名称列出来每一项带个删除按钮让用户能看到“看不见的那部分”是什么随时可以摘掉。这个小交互看起来不起眼但能显著降低“我明明没勾这么多啊”这类工单。如果选了清空记得一定要调clearSelection()不能只清selectedIds否则内部selection还留着翻页回来会发现莫名又勾上了。handleFilterTypeChange() { this.page 1; this.clearAll(); // 内部同时清 selectedIds 和 clearSelection this.fetchList(); }4.2 选中行被删掉之后的悬空 key批量操作之后经常要删掉几行删除成功后重新请求列表这时候selectedIds里可能还留着已经被删掉的 id。带上去提交后端要么报“数据不存在”要么默默忽略了前端显示“已选 3 条”但实际只处理了 2 条这种静默失败最难查。处理方式有两种。乐观一点的做法是在删除成功的回调里手动把 id 摘掉async handleDelete(row) { await api.delete(row.id); this.removeSelected(row.id); // 组件内部方法见上一节 this.fetchList(); }稳妥一点的做法是每次列表刷新之后做一次校验把不在当前全量集合里的 id 清掉。但服务端分页拿不到全量集合所以只能靠“显式删除时同步清理 提交前由后端校验”双保险。我在提交批量操作时会要求后端返回“成功列表和失败列表”前端根据失败列表反查哪些 id 有问题把它们从选中集合里摘掉并提示用户这样即使用户挂着悬空 id 也能自愈。4.3 抽成 mixin 复用后台系统里这种表格往往有七八个逐个复制粘贴维护成本太高。我一般抽成一个 mixinVue 2 项目里或者组合式函数Vue 3 / Element Plus 项目里。抽的时候有个原则mixin 只负责选中集合的维护和回填不负责请求。请求逻辑因页面而异塞进 mixin 里会导致它越来越臃肿最后变成一个什么都管、什么都管不好的巨型混入。// mixins/selectionMemory.js export default { data() { return { selectedIds: [], selectedBrief: [] }; }, methods: { syncSelection(selection) { this.selectedIds selection.map(row row.id); this.selectedBrief selection.map(row ({ id: row.id, name: row.name })); }, restoreSelection(list, tableRef) { if (!this.selectedIds.length || !tableRef) return; const idSet new Set(this.selectedIds.map(String)); list.forEach(row { const shouldSelect idSet.has(String(row.id)); const isSelected tableRef.selection.some( item String(item.id) String(row.id) ); if (shouldSelect ! isSelected) { tableRef.toggleRowSelection(row, shouldSelect); } }); }, removeSelectedId(id) { const idx this.selectedIds.indexOf(id); if (idx -1) this.selectedIds.splice(idx, 1); const idx2 this.selectedBrief.findIndex(item item.id id); if (idx2 -1) this.selectedBrief.splice(idx2, 1); }, resetSelection(tableRef) { this.selectedIds []; this.selectedBrief []; tableRef tableRef.clearSelection(); } } };页面里用的时候只需要在fetchList之后调用this.restoreSelection(this.list, this.$refs.table)在selection-change里调用this.syncSelection(val)剩下的都是业务自己的事。这样一套下来新页面接入这个功能大概五分钟比每次重新想一遍逻辑快得多。4.4 刷新页面、切换路由之后还要记住el-table的记忆只活在组件实例的生命周期里一旦路由切走再回来没开keep-alive或者用户按了 F5选中状态全没。这个需求在“用户需要跨页面选数据”的场景下确实存在比如从列表页勾选若干条去配置页做关联。实现方式是把selectedIds序列化存到sessionStoragekey 里带上页面标识和用户标识避免多个 Tab 之间互相污染。存的时候只存 id 数组别存整行数据一是体积大二是数据会过期。回填的时候在fetchList之后走同一套restoreSelection逻辑逻辑复用没有额外成本。const STORAGE_KEY order_list_selected_ids; saveToStorage() { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(this.selectedIds)); }, loadFromStorage() { try { const raw sessionStorage.getItem(STORAGE_KEY); this.selectedIds raw ? JSON.parse(raw) : []; } catch (e) { this.selectedIds []; } }这里有个小提醒用sessionStorage而不是localStorage。localStorage是永久的用户三天后回来打开这个页面发现莫名其妙勾着几条数据体验很怪。sessionStorage跟着标签页走关掉就清符合直觉。另外记得在用户点“清空选择”或者提交成功之后把 storage 里的也一并删掉否则会残留。5. 排错实录那些文档里不会写的坑5.1 高频问题速查表下面这张表是我这两年陆陆续续攒下来的基本覆盖了这类需求 90% 的故障现象。遇到问题先按现象对一遍能省掉大量瞎猜的时间。现象最可能的原因处理办法翻页回来勾选全丢reserve-selection写在了el-table上挪到el-table-column同时配row-key配了row-key还是丢row-key值类型不一致数字 vs 字符串统一String(row.id)检查后端字段类型勾一行结果勾了好几行存在重复的row-key常见于null、0给无 id 的行分配本地唯一 key已选数量对了但提交数据是旧的selection里是历史快照只提 id或提交前用最新数据合并表头全选后翻页第二页没勾全选只作用于当前页这是正常行为需要全局全选就在表格外单独做控件搜索后数量虚高被筛掉的选中项仍留在selection里搜索时决定清空或提供已选清单可视化删除数据后提交报错选中集合里有已删除的悬空 id删除成功回调里同步摘掉或后端返回失败列表反查调用toggleRowSelection没反应传入的 row 不在当前list里确保行对象来自当前list或用row-key重新查一次切换 Tab 后状态错乱多个表格共用同一个 ref 或 data每个表格独立实例、独立选中集合selection-change疯狂触发回填逻辑里没有做幂等判断toggleRowSelection前先比对当前是否已选5.2 我踩过的几个坑说出来让大家少走弯路第一个坑树形表格配reserve-selection。带父子层级的表格勾父节点会自动勾选所有子节点这在单页里没问题但跨页之后就乱了。因为子节点可能分散在不同页父节点在第一页被勾上第三页的子节点被toggleRowSelection回填时父节点的半选状态计算会出错表现出来就是“父节点显示勾选展开后发现子节点没勾全”。后来这个场景我彻底放弃reserve-selection改成全部自己维护 key 集合父子联动也自己算代码多了几十行但状态永远是对的。第二个坑动态列导致的 selection 列重建。有个页面允许用户自定义显示哪些列我用v-for渲染所有el-table-column包括那个typeselection的列。用户一改列配置v-for的 key 变了选择列被销毁重建selection直接重置。解决办法是把选择列从v-for里单独拎出来固定写在最前面不参与动态渲染。这个坑当时排查了很久因为现象是“偶尔丢”其实不是偶尔是用户改了列配置才丢。第三个坑在selection-change里做深拷贝。早期为了保险我在事件回调里写了JSON.parse(JSON.stringify(selection))想着快照一份不会互相影响。结果数据量上来之后用户勾了几百条每次勾选都卡一下因为深拷贝是同步阻塞的。后来改成只提取需要的几个字段map一下就完事流畅度立刻恢复。选中事件里只做轻量操作这是性能上的一条铁律。第四个坑clearSelection之后selection-change不一定按预期触发。有些版本里清空选中后事件触发的时机和调用顺序有关系如果在clearSelection之后立刻读this.selectedIds可能拿到的是旧值。稳妥写法是把清空选中集合的动作放在事件回调里统一做而不是依赖调用的同步性。5.3 上线前值得自己过一遍的自测清单代码写完别急着提交下面这几条我会在每次做完这类功能后手动过一遍成本十分钟但能挡掉大部分线上问题。第一勾选第一页三条翻到最后一页再翻回来三条是否还在表头是否显示半选。第二勾选后点搜索确认选中数量和列表内容是否符合预期保留还是清空得跟产品对齐。第三勾选几条之后删掉其中一条看“已选”数字是否自动减一提交时是否还带着已删除的 id。第四把浏览器窗口缩小到出现横向滚动确认固定列下的勾选框显示正常。第五连续快速翻页五次看是否有重复请求导致的选中状态闪回。第六清空选择后立刻翻页确认不会莫名其妙又勾上。这六条覆盖了绝大多数回归问题尤其是第五条的快速翻页很容易暴露出请求竞态导致的回填错乱——老请求后返回把新页面的勾选状态覆盖了。真遇到这种情况就在请求里带上一个自增的requestId回来时比对一下不是最新的一次就直接丢弃。这套东西说到底没什么高深的技术难的是把每个边界都想清楚。我个人的体会是遇到“选中记忆”这种看起来简单的需求先别急着写代码花五分钟把“谁会被选中、选中之后会做什么、什么情况下选中会失效”这三件事在纸上列一遍后面的返工会少很多。最后再分享一个实用的小习惯在组件里加一个开发环境专用的调试按钮一键打印当前selectedIds、selection里的对象和当前页数据联调时比翻控制台快得多联调完记得用环境变量关掉就行。