DataTables Select插件与jQuery事件处理器执行时机问题
最近我查了不少资料和jQuery API文档,确认click()直接绑定和on('click')事件委托除了委托特性外没别的差异,但在搭配DataTables和它的Select插件使用时,却碰到了一个头疼的执行顺序问题——委托的回调总是在DataTables的选择逻辑之前跑,导致拿不到最新的选中状态。
先给大家看看我复现问题的代码:
复现代码
HTML结构
<div class='panel'> <table class='data-table'> <thead> <tr> <th>Column 1</th> <th>Column 2</th> </tr> </thead> <tbody> <tr data-id='1'> <td>Row 1, 1</td> <td>Row 1, 2</td> </tr> <tr data-id='2'> <td>Row 2, 1</td> <td>Row 2, 2</td> </tr> <tr data-id='3'> <td>Row 3, 1</td> <td>Row 3, 2</td> </tr> </tbody> </table> </div>
JavaScript事件绑定
// 直接绑定click事件 $('table tbody tr').click(function(){ console.log("Click(): " + $('table tr.selected').data('id')); }); // 事件委托绑定click $('table').on('click', 'tbody tr', function() { console.log("On(Click()): " + $('table tr.selected').data('id')); });
奇怪的现象
本来以为两种绑定方式输出应该完全一致,但实际测试结果却差得远:
- 第一次点
data-id=1的行,控制台输出:On(Click()): undefined Click(): 1 - 从第一行点到第二行时,输出变成:
明显能看出来,事件委托的回调是在DataTables给行加On(Click()): 1 Click(): 2selected类之前执行的,而直接绑定的click()则是在选择逻辑完成后才跑。但因为数据集太大,必须用事件委托,所以得想办法让委托的回调也能拿到最新的选中状态。
补充说明:虽然用
this能直接拿到当前点击的行,但再次点击同一行取消选择时,两个函数都应该返回undefined,所以还是得依赖选中状态的判断。
问题根源
这其实是事件绑定机制和DataTables插件的事件处理顺序导致的:
- 直接绑定
click()的tr是页面初始化时就存在的DOM节点,DataTables的Select插件会先给这些tr绑定自己的选择逻辑事件,所以当点击触发时,插件先完成加/减selected类的操作,再轮到我们的回调执行。 - 而事件委托是把监听绑定在父元素
table上,事件冒泡到table节点时才触发我们的回调,但这时候DataTables的选择逻辑还没跑完(插件的事件处理是在tr节点上,理论上冒泡顺序应该是先tr后table,但实际因为插件的事件绑定时机或者内部处理逻辑,导致我们的委托回调先执行了)。
靠谱的解决方案
方案1:用DataTables官方提供的选择事件(最推荐)
DataTables的Select插件专门提供了select.dt和deselect.dt事件,这些事件是在插件完成选择/取消选择逻辑之后才触发的,完美解决顺序问题:
// 监听选择和取消选择事件 $('.data-table').on('select.dt deselect.dt', function(e, dt, type, indexes) { if (type === 'row') { // 获取选中的行节点 var selectedRows = dt.rows(indexes).nodes(); if (selectedRows.length > 0) { console.log("Selected ID: " + $(selectedRows).data('id')); } else { console.log("Selected ID: undefined"); } } });
这种方式不仅能准确拿到选中状态,还能自然处理取消选择的场景,完全符合需求。
方案2:用setTimeout延迟执行委托回调
如果不想改绑定方式,也可以利用JavaScript的事件循环机制,把回调逻辑延迟到当前同步任务都跑完之后:
$('table').on('click', 'tbody tr', function() { // 延迟到下一个事件循环周期执行 setTimeout(() => { console.log("On(Click()): " + $('table tr.selected').data('id')); }, 0); });
setTimeout(..., 0)会把代码放到微任务队列里,等当前所有同步执行的代码(包括DataTables的选择逻辑)都完成后再执行,这样就能拿到最新的selected类状态了。
方案3:通过DataTables API直接判断选中状态
也可以不依赖selected类,直接用DataTables的API来判断当前行的选中状态:
$('table').on('click', 'tbody tr', function() { // 获取DataTables实例 var dt = $('.data-table').DataTable(); // 获取当前点击的行对象 var currentRow = dt.row(this); // 判断是否选中 if (currentRow.selected()) { console.log("On(Click()): " + $(this).data('id')); } else { console.log("On(Click()): undefined"); } });
不过要注意,点击瞬间的选中状态可能还没更新,所以最好搭配setTimeout一起用,或者直接用方案1的官方事件更稳妥。
总结
最推荐的肯定是方案1,毕竟是官方提供的事件,完全避开了事件顺序的坑,代码也更规范。如果只是临时调试或者不想大改代码,方案2也是个快速解决的办法。
内容的提问来源于stack exchange,提问作者ashok19r91d

