Vue3表格点击client_txid二次排序触发insertBefore报错的原因与解决
问题
在Vue3+TypeScript项目中实现表格排序功能,点击client_txid表头可完成升序排序,但再次点击该表头切换排序方向时,控制台抛出错误:
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'insertBefore')
完整代码如下:
<script setup lang="ts"> import { ref, computed, watch } from 'vue' const data = ref([ { header: { id_file: '11', id_client: '0', client_txid: '01TF 49608', spa_status: 'A', }, try_log: [ { hac_id: '184', exec_date: '2024-08-05T20:32:25.579Z', api_env: 'PRODUCCION_REAL', }, { hac_id: '168', exec_date: '2024-08-05T18:54:57.535Z', api_env: 'PRODUCCION_REAL', }, ], }, { header: { id_file: '11', id_client: '0', client_txid: '01MN 177338', spa_status: 'TIMEOUT', }, try_log: [ { hac_id: '184', exec_date: '2024-08-05T20:32:25.579Z', api_env: 'PRODUCCION_REAL', }, { hac_id: '168', exec_date: '2024-08-05T18:54:57.535Z', api_env: 'PRODUCCION_REAL', }, ], }, { header: { id_file: '11', id_client: '0', client_txid: '01MN 177338', spa_status: 'PAGADA', }, try_log: [ { hac_id: '167', exec_date: '2024-08-05T18:54:55.780Z', api_env: 'PRODUCCION_REAL', }, ], }, { header: { id_file: '500', id_client: '850', client_txid: 'TRX-VE4586-T3', spa_status: '', }, try_log: [ { hac_id: '166', exec_date: '2024-08-05T18:54:51.719Z', api_env: 'PRODUCCION_REAL', }, ], }, ]) const page = ref(1) const itemsPerPage = ref(10) const sortBy = ref<string>('') const sortDirection = ref<string>('asc') const filteredData = ref(data.value) const filters = ref({ search: '', id_file: '', }) const normalizeValue = (value: any) => { if (typeof value === 'string') { return value.toLowerCase() } if (typeof value === 'number' || typeof value === 'boolean') { return value.toString() } return value } function sortTable(column: string) { if (sortBy.value === column) { sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc' } else { sortBy.value = column sortDirection.value = 'asc' } const modifier = sortDirection.value === 'asc' ? 1 : -1 const sortedData = [...filteredData.value].sort((a, b) => { const aValue = normalizeValue(a.header[column] ?? ''); const bValue = normalizeValue(b.header[column] ?? ''); if (!aValue && !bValue) return 0; // Both null, no change in order if (!aValue) return 1; // a is null, move b before a (descending) if (!bValue) return -1; // b is null, move a before b (ascending) // Existing comparison logic if (aValue < bValue) return -1 * modifier; if (aValue > bValue) return 1 * modifier; return 0 }) filteredData.value = sortedData } watch( filters, () => { page.value = 1 filteredData.value = data.value.filter((item) => { return Object.values(item.header).some((value) => String(value).toLowerCase().includes(filters.value.search.toLowerCase()) ) }) }, { immediate: true } ) const paginatedData = computed(() => { if (itemsPerPage.value === 0) return filteredData.value const start = (page.value - 1) * itemsPerPage.value const end = start + itemsPerPage.value return filteredData.value.slice(start, end) }) watch(filters, () => { page.value = 1 }) </script> <template> <div> <div class="list-view-toolbar"> <div class="search-field"> <VField> <VControl icon="feather:search" style="margin: 0" > <input v-model="filters.search" class="input custom-text-filter" placeholder="Search..." > </VControl> </VField> </div> </div> </div> <div class="datatable-wrapper"> <div class="table-container" style="overflow: auto" > <table class="table datatable-table is-fullwidth"> <thead> <tr> <th class="col-estatus" align="center" @click="sortTable('id_client')" > id_client <span v-if="sortBy === 'id_client'"> <i v-if="sortDirection === 'asc'" class="iconify" data-icon="feather:chevron-up" aria-hidden="true" /> <i v-else class="iconify" data-icon="feather:chevron-down" aria-hidden="true" /> </span> </th> <th class="col-estatus" align="center" @click="sortTable('client_txid')" > client_txid <span v-if="sortBy === 'client_txid'"> <i v-if="sortDirection === 'asc'" class="iconify" data-icon="feather:chevron-up" aria-hidden="true" /> <i v-else class="iconify" data-icon="feather:chevron-down" aria-hidden="true" /> </span> </th> <th class="col-estatus" align="center" @click="sortTable('id_file')" > id_file <span v-if="sortBy === 'id_file'"> <i v-if="sortDirection === 'asc'" class="iconify" data-icon="feather:chevron-up" aria-hidden="true" /> <i v-else class="iconify" data-icon="feather:chevron-down" aria-hidden="true" /> </span> </th> </tr> </thead> <tbody> <tr v-for="(item, index) in paginatedData" :key="index" > <td class="col-id-cliente" align="center" > {{ item.header.id_client }} </td> <td class="col-id-beneficiario" align="center" > {{ item.header.client_txid }} </td> <td class="col-id-cliente" align="center" > {{ item.header.id_file }} </td> </tr> </tbody> </table> </div> </div> </template>
报错原因
- 行key使用索引引发DOM复用异常:表格行的
:key绑定了index,排序后数据顺序改变,Vue会复用原有DOM节点,但索引对应的内容已变化,结合Iconify动态图标组件,DOM更新时会出现节点引用丢失,触发insertBefore的null错误。 - 直接修改filteredData导致响应式更新冲突:每次排序直接替换
filteredData.value,分页计算与排序操作的响应式更新队列冲突,引发DOM操作目标节点不存在的问题。
修复方法
1. 替换行key为唯一标识
将表格行的:key从index改为数据项的唯一组合值,确保Vue能准确识别每个行节点:
<tr v-for="(item, index) in paginatedData" :key="`${item.header.id_file}-${item.header.client_txid}`" >
2. 用计算属性处理排序逻辑,避免直接修改filteredData
重构排序逻辑为计算属性,让响应式更新更稳定:
// 新增排序后的计算属性 const sortedAndFilteredData = computed(() => { let result = [...filteredData.value]; if (sortBy.value) { const modifier = sortDirection.value === 'asc' ? 1 : -1; result.sort((a, b) => { const aValue = normalizeValue(a.header[sortBy.value] ?? ''); const bValue = normalizeValue(b.header[sortBy.value] ?? ''); if (!aValue && !bValue) return 0; if (!aValue) return 1; if (!bValue) return -1; if (aValue < bValue) return -1 * modifier; if (aValue > bValue) return 1 * modifier; return 0; }); } return result; }); // 修改分页计算属性依赖排序后的数据 const paginatedData = computed(() => { if (itemsPerPage.value === 0) return sortedAndFilteredData.value; const start = (page.value - 1) * itemsPerPage.value; const end = start + itemsPerPage.value; return sortedAndFilteredData.value.slice(start, end); }); // 简化sortTable函数,仅更新排序状态 function sortTable(column: string) { if (sortBy.value === column) { sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'; } else { sortBy.value = column; sortDirection.value = 'asc'; } }
3. 合并重复的watch监听
删除重复的filters监听,保留一个即可:
watch( filters, () => { page.value = 1; filteredData.value = data.value.filter((item) => { return Object.values(item.header).some((value) => String(value).toLowerCase().includes(filters.value.search.toLowerCase()) ); }); }, { immediate: true } );
内容的提问来源于stack exchange,提问作者alex
相关产品推荐
相关产品推荐

