How Adding a Row Key Fixed a Mysterious SortableJS Drag‑Drop Bug in Vue
The article walks through a puzzling drag‑and‑drop sorting bug in a Vue admin table, explains why the virtual DOM diverged from the real DOM, and shows that adding a unique row‑key to the el‑table element resolves the issue while highlighting best coding practices.
1. Preface
Learning through bugs can turn frustration into deep knowledge; the author shares a recent mysterious drag‑and‑drop bug encountered while working on a Vue admin list.
bug 越多,能力越大。
After many late‑night debugging sessions, the author decided to document the problem and its solution.
2. What is the problem
Business scenario: a management backend list needs drag‑and‑drop sorting. The requirement looks simple but the difficulty lies in handling the list data during drag operations.
Instead of using a heavyweight library like vue‑draggable, the lightweight SortableJS was chosen.
1. Using SortableJS
SortableJS is lightweight, but its documentation is sparse, making configuration sometimes tricky.
<code><template>
<div>
<!-- 表单 table -->
<el-table v-loading="loading" :data="currentLessonList" class="p-course-classes-wrapper--class-table">
<el-table-column prop="lessonName" label="课时名称"></el-table-column>
<el-table-column prop="lessonCode" label="课时 ID"></el-table-column>
<el-table-column prop="gmtCreated" label="添加时间">
<template slot-scope="scope">
<span>{{ formatTime(scope.row.gmtCreated )}}</span>
</template>
</el-table-column>
<el-table-column prop="surveyName" label="随堂测试">
<template slot-scope="scope">
<span>{{ scope.row.surveyName || '--'}}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
...
activated () {
// 初始化排序列表
this.$nextTick(() => {
const that = this
const tbody = document.querySelector('.el-table__body-wrapper tbody')
this.sortObj = new Sortable(tbody, {
animation: 150,
sort: true,
disabled: !that.isCanDrag,
onEnd: async function (evt) {
// SortableJS 不改变数据的实际顺序,但是传递新旧索引值,需要开发者手动根据索引值改变数据顺序
that.currentLessonList.splice(evt.newIndex, 0, that.currentLessonList.splice(evt.oldIndex, 1)[0])
that.currentLessonList = that.currentLessonList.map((item, index) => {
return {
...item,
sort: index + 1
}
})
await that.updateLessonsOrder()
}
})
})
}
}
</script></code>The code shows that after initializing Sortable on the table body, the
onEndhandler manually reorders the underlying data array to match the new DOM order.
2. Mysterious issue
After implementing the above, a strange bug appeared: swapping the first and third rows in the UI did not reflect the same order in the data array.
The visual DOM order and the model data became inconsistent.
3. Analysis
The view layer had changed, but the model layer had not been updated correctly. The only data‑reordering code resides in the
onEndfunction, yet debugging showed the array remained unchanged.
4. Solution
The fix was to add a unique
row-keyattribute to the
el-tablecomponent, allowing Vue to track each row properly.
<code><template>
<el-table :data="currentLessonList" :row-key="row => row.pkId">
....
</el-table>
</template></code>One line of code resolved the afternoon‑long mystery.
3. Exploring the root cause
The bug stemmed from a mismatch between the Virtual DOM and the real DOM after Sortable moved elements.
1. Virtual DOM vs Real DOM
Before modern frameworks, developers manipulated real DOM directly (e.g., jQuery). Frameworks like Vue and React introduced a Virtual DOM, allowing developers to work with a lightweight representation that Vue diffs and patches to the real DOM.
2. Concrete example
Assume the list array is:
<code>let tableData = ['A', 'B', 'C', 'D']</code>Rendered real DOM:
<code>let tableData_dom = ['$A', '$B', '$C', '$D']</code>Corresponding Virtual DOM structure:
<code>let tableData_vm = [
{el: '$A', data: 'A'},
{el: '$B', data: 'B'},
{el: '$C', data: 'C'},
{el: '$D', data: 'D'}
];</code>After dragging, the real DOM becomes:
<code>['$B', '$A', '$C', '$D']</code>Sortable only changes the real DOM; the Virtual DOM remains unchanged, still reflecting the original order. When the
onEndhandler updates the data array to match the new DOM order, Vue’s diff algorithm sees a mismatch between the Virtual DOM and the updated data, causing it to re‑patch the real DOM and produce the observed bug.
Drag real DOM → modify data array → patch algorithm updates real DOM
3. Further investigation
Understanding the diff algorithm and the importance of a unique key explains why a single
row-keyline fixes the issue.
4. Self‑reflection
1. Coding standards
Using indexes as keys is discouraged; a proper unique key prevents subtle bugs like this and promotes better long‑term code health.
2. Knowing why
Simply fixing bugs without grasping the underlying mechanisms leads to shallow knowledge; deeper insight into frameworks’ internals yields more robust solutions.
5. Summary
The article presented a real‑world drag‑and‑drop bug, described the business context, detailed the solution (adding a row‑key), explored the root cause (Virtual DOM vs real DOM inconsistency), and highlighted the importance of coding standards and deeper framework understanding.
6. References
深入浅出 Vue 中的 key 值: https://juejin.cn/post/6844903865930743815
Vue 中使用 SortableJS: https://www.jianshu.com/p/d92b9efe3e6a
virtual‑dom (Vue 实现)简析: https://segmentfault.com/a/1190000010090659
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.