资讯动态

JQuery单行变色表格实战:动态渲染与性能优化指南

发布时间:2026/10/15 17:30:44 来源:尧图企业网站定制
1. 为什么一个单行变色表格值得单独拿出来讲很多人看到JQuery实现单行变色表格这个标题第一反应是这有什么好写的不就是tr:odd加个背景色吗我一开始也这么想。但真正在项目里落地过的人会知道一个看起来只有三行代码的需求背后牵扯的是可维护性、性能边界、交互冲突和浏览器渲染细节这一整套东西。尤其是当表格从静态展示变成动态渲染、从几十行变成几千行、从纯展示变成带勾选和悬浮效果的时候最初那三行代码往往会变成一堆互相打架的样式覆盖。这篇内容我想聊的不是怎么写出变色效果而是怎么写出一个在真实项目里不会给你添麻烦的变色表格。核心关键词就是JQuery、单行变色、表格隔行换色、斑马纹、动态渲染、事件委托。适合的人群很明确正在用JQuery维护老项目的前端同学、需要快速给后台管理系统加表格样式的新手、以及想搞清楚为什么我的变色在动态数据下失效了的开发者。先说清楚这个功能到底解决什么问题。表格数据一多人眼在横向扫描一行的时候很容易串行尤其是列数超过六列、单元格内容又比较密集的时候。隔行变色业内常叫斑马纹利用的是人眼对明暗交替的敏感度让每一行的边界在视觉上被强化从而降低读错行的概率。这不是审美问题是可读性和效率问题。所以它虽然简单但属于后台系统里性价比极高的一类优化。我见过太多项目把这段逻辑写死在页面加载的那一刻结果接口数据一回来表格是异步渲染的变色全没了。也见过用CSS的nth-child写死结果表格中间插了一行暂无数据的提示行整个斑马纹错位。这些问题都不是会不会写的问题而是有没有想过边界的问题。下面我会从原理、实现、动态场景、性能、踩坑几个角度把这个小功能彻底拆开讲一遍。2. 隔行变色的三种实现路径与选型逻辑2.1 纯CSS方案最省事但边界最硬纯CSS做隔行变色核心就是选择器。最经典的写法是给表格的tbody下的tr用:nth-child(odd)和:nth-child(even)分别设置背景色。这种写法的好处是零JS开销浏览器渲染时直接应用性能最好而且不依赖任何库。/* 纯CSS隔行变色 */ .table-zebra tbody tr:nth-child(odd) { background-color: #ffffff; } .table-zebra tbody tr:nth-child(even) { background-color: #f7f9fc; }但它的硬伤也很明显。第一:nth-child是按DOM中的物理位置计数的一旦你在表格里插入了一个tr作为分组标题行或者加载中占位行后面的奇偶就全部错位。第二它无法响应用户点击某行高亮这类动态状态因为CSS管不了状态切换后的优先级问题——你点击行加了个.active类结果被nth-child的背景色盖住了这种覆盖问题非常常见。第三如果表格是分页异步渲染的每次重新渲染DOMCSS虽然会自动重新计算但如果你同时有JS在操作行样式两者就会打架。所以纯CSS适合的场景是静态表格、结构稳定、没有交互高亮需求。一旦超出这个范围就得考虑JS介入。2.2 JQuery的:odd/:even选择器直观但有个大坑JQuery提供了:odd和:even伪类选择器写起来非常符合直觉// 注意JQuery的:even和:odd是从0开始计数的 $(.table-zebra tbody tr:even).css(background-color, #ffffff); $(.table-zebra tbody tr:odd).css(background-color, #f7f9fc);这里有个极其容易踩的坑JQuery的:even和:odd索引是从0开始的而CSS的:nth-child是从1开始的。也就是说JQuery里的:even对应的是第1、3、5行索引0、2、4而CSS里的:nth-child(even)对应的是第2、4、6行。这两个even的含义完全相反。我见过不止一个项目里设计师给的稿子是第一行白色第二行灰色结果开发用JQuery的:odd去设置出来的效果正好反了然后两个人对着屏幕争论了半天。另外:odd/:even选择器在JQuery内部并不是原生CSS选择器它需要JQuery遍历所有匹配元素后按索引筛选属于非标准选择器性能上比原生选择器差。当表格行数上千时这个差距会变得可感知。所以它适合中小型表格不适合大数据量场景。2.3 手动遍历加类名啰嗦但最可控第三种方式是老老实实用.each()遍历根据索引手动加类名$(.table-zebra tbody tr).each(function(index) { // 索引从0开始偶数索引加偶数类 $(this).removeClass(row-odd row-even) .addClass(index % 2 0 ? row-even : row-odd); });看起来最笨但它有三个别人比不了的优势。第一逻辑完全透明你想跳过某一行、想根据数据内容决定颜色、想在变色基础上叠加其他状态都只需要改这个循环里的判断条件。第二类名和样式分离颜色定义在CSS里JS只负责打标记后期改配色不用动JS。第三动态重排时最容易复用你把这个遍历封装成一个函数数据一变就重新调用一次逻辑清晰。我个人的选型习惯是这样的如果表格是纯静态的、行数在50以内直接用CSS如果是JQuery项目、行数中等、需要一点动态效果用.each()加类名:odd/:even我基本只在写一次性demo的时候用正式项目里会刻意避开因为那个索引起点问题太容易埋雷。方案性能动态适应性可维护性推荐场景纯CSS nth-child最高差中静态表格JQuery :odd/:even中中低临时demoeach加类名中最好高正式项目3. 从零写一个能扛住动态数据的变色表格3.1 基础结构把样式和逻辑彻底分开先搭HTML骨架。这里我刻意用thead和tbody分开因为变色只针对数据行表头不应该参与隔行逻辑。这一点很多人会忽略直接把变色写在tr上结果表头也被算进去了第一行数据的颜色就跟表头撞了。table classtable-zebra iddataTable thead tr th序号/th th名称/th th状态/th th更新时间/th /tr /thead tbody idtableBody !-- 数据行由JS渲染 -- /tbody /tableCSS部分我把颜色抽成类名同时预留了悬浮和选中两种状态的样式。注意这里用background-color而不是background避免把背景图之类的属性一起覆盖掉。.table-zebra { width: 100%; border-collapse: collapse; } .table-zebra th, .table-zebra td { padding: 10px 12px; border-bottom: 1px solid #e8ecf1; text-align: left; } .table-zebra thead th { background-color: #eef2f7; font-weight: 600; } /* 隔行颜色 */ .table-zebra tbody tr.row-even { background-color: #ffffff; } .table-zebra tbody tr.row-odd { background-color: #f7f9fc; } /* 悬浮态优先级要高于隔行色 */ .table-zebra tbody tr:hover { background-color: #eaf3ff; } /* 选中态优先级最高 */ .table-zebra tbody tr.row-active { background-color: #dcebff; }这里有个优先级设计的经验隔行色、悬浮色、选中色三者的优先级必须拉开。CSS里后定义的、选择器更具体的会覆盖前面的。tr:hover和tr.row-active都比tr.row-even更具体多了伪类或类名所以能正常覆盖。如果你把隔行色写在最后那悬浮效果就会被吃掉。这个顺序不是随便排的是踩过坑之后固定下来的。3.2 渲染函数数据变了就重新上色核心逻辑封装成一个函数接收数据数组先渲染行再统一上色。这样做的好处是渲染和上色是两个独立步骤任何一步出问题都好定位。function renderTable(list) { var $tbody $(#tableBody); $tbody.empty(); if (!list || list.length 0) { // 空数据占位行注意给它单独的类不参与隔行 $tbody.append(tr classrow-emptytd colspan4暂无数据/td/tr); return; } var html ; for (var i 0; i list.length; i) { var item list[i]; html tr>// 删除某一行 function deleteRow(id) { $(#tableBody tr[data-id id ]).remove(); applyZebraStriping(); // 关键删完必须重新上色 } // 在末尾追加一行 function appendRow(item) { var $tbody $(#tableBody); var index $tbody.find(tr).not(.row-empty).length; var $tr $(tr).attr(data-id, item.id) .append(td (index 1) /td) .append(td item.name /td) .append(td item.status /td) .append(td item.updateTime /td); $tbody.append($tr); applyZebraStriping(); // 同样要重新上色 }4.2 一个更隐蔽的坑排序之后颜色全乱比增删更容易被忽略的是排序。用户点了表头排序行的顺序变了但类名还跟着原来的行跑。这时候斑马纹看起来还在但实际上是错乱的——因为颜色不再交替了。解决办法一样排序完成后重新调用上色函数。还有一种情况是前端分页。如果表格数据是前端切片显示的翻页时你重新渲染了当前页的数据那上色函数自然会被调用没问题。但如果你用的是隐藏/显示行的方式做分页把非当前页的行display:none那就要注意:nth-child和JQuery的:odd都是按所有匹配元素计数的隐藏的行依然占索引斑马纹会错位。这种情况下必须用.each()配合可见性判断或者干脆改成重新渲染。提示只要表格支持增、删、排序、筛选中的任意一种操作隔行变色就不能是一次性的必须封装成可重复调用的函数。这是这个功能从能用到可靠的分水岭。4.3 用事件委托处理动态行的点击动态渲染的行点击事件不能用普通的.click()绑定因为绑定的时候行还不存在。必须用事件委托把监听挂在父级tbody上$(#tableBody).on(click, tr, function() { // 排除占位行 if ($(this).hasClass(row-empty)) return; // 单选高亮 $(this).addClass(row-active).siblings().removeClass(row-active); });这里.siblings()会把同级的其他行都取消高亮实现单选效果。注意row-active的样式优先级要高于隔行色前面CSS里已经处理过了。如果你希望支持多选就把siblings().removeClass那行去掉改成toggleClass。事件委托的另一个好处是性能。如果表格有1000行用.click()逐个绑定就是1000个事件监听器内存占用高用委托只挂1个监听器在tbody上事件冒泡上来再判断目标内存友好得多。这也是JQuery在动态列表场景下最值得用的特性之一。5. 大数据量下的性能取舍与优化手段5.1 一千行以上瓶颈在哪里当表格行数上到几千行隔行变色本身不是瓶颈DOM操作和重排才是。每次调用applyZebraStriping().each()遍历所有行每行做removeClass和addClass这会触发多次样式重算。如果这个函数被频繁调用比如每次删一行都调累积起来就会卡。优化的第一个手段是减少类名操作次数。与其每行都removeClass再addClass不如先判断当前类名是否已经正确正确就跳过function applyZebraStriping() { $(#tableBody tr).not(.row-empty).each(function(index) { var $tr $(this); var targetClass index % 2 0 ? row-even : row-odd; if (!$tr.hasClass(targetClass)) { $tr.removeClass(row-even row-odd).addClass(targetClass); } }); }这个判断看起来微不足道但在只删了一行、大部分行颜色没变的场景下能省掉绝大部分DOM操作。实测在2000行的表格上删除一行后重新上色的耗时能从几十毫秒降到几毫秒。5.2 用文档片段批量操作如果表格是整体重新渲染的拼接HTML字符串再一次性html()写入比逐行append()快得多。因为逐行append会触发多次重排而一次性写入只触发一次。前面renderTable里用的就是字符串拼接加html()这是JQuery时代处理批量DOM的标准做法。// 推荐字符串拼接后一次性写入 var html ; for (var i 0; i list.length; i) { html tr.../tr; } $tbody.html(html); // 不推荐逐行append行数多时明显卡顿 for (var i 0; i list.length; i) { $tbody.append(tr.../tr); }5.3 什么时候该放弃JQuery方案说句实在话如果表格行数稳定超过5000行或者需要频繁的局部更新JQuery这套全量遍历上色的思路就不太合适了。这时候更合理的做法是虚拟滚动——只渲染可视区域内的几十行滚动时动态替换内容。虚拟滚动下隔行变色只需要对当前渲染的那几十行做开销恒定跟总数据量无关。但虚拟滚动是另一个话题了实现复杂度高不少。我的建议是行数在2000以内JQuery方案完全够用超过这个量级先考虑分页分页解决不了再考虑虚拟滚动。不要为了一个隔行变色去上重型方案那是本末倒置。数据量级推荐方案上色方式50行以内纯CSSnth-child50-2000行JQuery渲染each加类名2000行以上分页或虚拟滚动仅对可视行上色6. 那些文档里不会写的实操细节6.1 边框和背景色的叠加问题border-collapse: collapse下单元格的边框和行的背景色会有一个渲染顺序问题。某些浏览器里如果行背景色是半透明的边框会被背景色盖住一部分看起来边框断断续续。解决办法是行背景色用不透明的纯色或者把边框设在单元格上而不是行上。我前面CSS里把border-bottom设在th和td上就是为了避开这个坑。6.2 打印时的颜色丢失后台系统经常有打印表格的需求。浏览器默认不打印背景色所以打印出来斑马纹全没了表格变成一片白可读性反而下降。如果打印是刚需得加media print样式并且提示用户在打印设置里勾选背景图形media print { .table-zebra tbody tr.row-odd { background-color: #f2f2f2 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } }print-color-adjust: exact是告诉浏览器强制打印背景色但不同浏览器支持程度不一样所以最稳妥的还是提醒用户手动开启。6.3 悬浮态在移动端的尴尬tr:hover在桌面端很好用但在触屏设备上点击之后悬浮态会粘在那一行上不消失看起来像选中了但又没选中。如果项目要兼容移动端建议用媒体查询把悬浮态限制在支持悬浮的设备上media (hover: hover) { .table-zebra tbody tr:hover { background-color: #eaf3ff; } }这样触屏设备就不会触发悬浮样式避免状态混乱。6.4 别用行内样式用类名我见过一些代码直接用.css(background-color, ...)给行设置行内样式。这样做的后果是行内样式优先级极高后面你想用类名覆盖比如选中高亮就覆盖不掉只能再用.css()去改越写越乱。坚持用类名样式全部交给CSS管JS只负责切换类这是保持代码干净的关键。7. 一套可以直接抄走的完整实现把前面的东西整合一下给一份完整可运行的代码。HTML结构、CSS、JS三部分复制到本地就能跑。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title隔行变色表格/title style .table-zebra { width: 100%; border-collapse: collapse; font-size: 14px; } .table-zebra th, .table-zebra td { padding: 10px 12px; border-bottom: 1px solid #e8ecf1; text-align: left; } .table-zebra thead th { background-color: #eef2f7; font-weight: 600; } .table-zebra tbody tr.row-even { background-color: #ffffff; } .table-zebra tbody tr.row-odd { background-color: #f7f9fc; } .table-zebra tbody tr.row-active { background-color: #dcebff; } .table-zebra tbody tr.row-empty td { text-align: center; color: #999; } media (hover: hover) { .table-zebra tbody tr:hover { background-color: #eaf3ff; } } /style /head body table classtable-zebra iddataTable thead trth序号/thth名称/thth状态/thth更新时间/th/tr /thead tbody idtableBody/tbody /table script srchttps://code.jquery.com/jquery-3.6.0.min.js/script script $(function() { var mockData [ { id: 1, name: 任务A, status: 进行中, updateTime: 2024-05-01 }, { id: 2, name: 任务B, status: 已完成, updateTime: 2024-05-02 }, { id: 3, name: 任务C, status: 待开始, updateTime: 2024-05-03 }, { id: 4, name: 任务D, status: 进行中, updateTime: 2024-05-04 }, { id: 5, name: 任务E, status: 已完成, updateTime: 2024-05-05 } ]; function applyZebraStriping() { $(#tableBody tr).not(.row-empty).each(function(index) { var $tr $(this); var targetClass index % 2 0 ? row-even : row-odd; if (!$tr.hasClass(targetClass)) { $tr.removeClass(row-even row-odd).addClass(targetClass); } }); } function renderTable(list) { var $tbody $(#tableBody); $tbody.empty(); if (!list || list.length 0) { $tbody.append(tr classrow-emptytd colspan4暂无数据/td/tr); return; } var html ; for (var i 0; i list.length; i) { var item list[i]; html tr>

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑