☰
vxe-table 数据加载与更新全解析:从声明式绑定到 loadData、reloadData 实战避坑
2026/9/26 21:20:20 网站建设 项目流程

做后端管理系统的人,十有八九都用过 vxe-table 这类重度表格组件。实话说,它比普通 el-table 能扛的场景多太多了:大数据量虚拟滚动、单元格编辑、树形表格、固定列、自定义渲染……但用起来门槛也高,尤其是"数据到底怎么喂进去、怎么改"这个问题,官方文档散落在 API 里,新手很容易把loadData、reloadData、setData、直接改:data绑定数组这几种方式混在一起用,结果表格不刷新、行状态丢光、loading 卡死,各种玄学问题全冒出来。

这篇文章我就围绕 vxe-table 加载数据的几种方式,以及更新数据的常见用法,把数据流这件事彻底讲透。内容适合正在用 vxe-table 做中后台项目、或者在 el-table 和 vxe-table 之间纠结要不要切换的兄弟参考,我会直接给代码、给选择建议、给排坑经验,不绕弯子。

1. 先把数据来源理清楚:vxe-table 的数据体系

1.1 外部响应式数据与表格内部数据并存

vxe-table 有一个很容易被忽略的设计:它允许你完全不声明data属性,也能通过实例方法管理数据;同时它又支持v-model:data双向绑定外部数组。这就意味着表格内部有一套自己的数据维护逻辑,外部数组只是它的一个"受控镜像"。

这句话怎么理解?我举个例子。你在模板里写了:

<vxe-table :data="tableData"></vxe-table>

此时表格在渲染时会读取tableData的值,并把这份数据作为当前表格数据。你通过ref调用loadData或insert时,表格内部的记录会变化,同时会触发update:data事件,把最新的数组同步回tableData。反过来,你直接给tableData.value赋一个全新的数组,表格感知到data变化后也会重新渲染。

所以这里存在两条操作路径:

  • 外部驱动:修改tableData变量本身,表格被动刷新,通常用于查询、筛选、重置这种整包换数据的场景。
  • 内部驱动:调用表格实例方法,比如loadData、insert、remove、setRow,表格自己维护数据,并通过事件把结果回写到外部变量。

很多人的困惑就出在这两条路径交叉使用上。你在外部改了数组,回头又用reloadData加载同一份数据,行状态、展开状态、选中状态全被重置了,然后你开始怀疑是 vxe-table 的 bug——其实这属于没有理清"你的数据入口到底是谁"。

1.2 声明式绑定和命令式调用怎么选

我的项目经验是,先定一个数据入口策略,不要一会儿走外部数组、一会儿走实例方法。如果你的表格比较简单,没有大量内部操作,走声明式绑定就够了,即只维护tableData,增删改都靠数组的push、splice、filter完成。但如果表格开启了行编辑、树形结构、勾选联动,内部方法往往比你自己操作数组更靠谱,因为 vxe-table 的方法内部会处理很多状态同步细节。

拿一个很典型的场景来说:默认分页查询,接口返回{ rows, total },查询按钮点击后重新拉数据,这个时候用外部驱动非常舒服:

const tableData = ref([]) const loading = ref(false) const loadPage = async () => { loading.value = true try { const { rows } = await fetchUserList({ pageNo: 1, pageSize: 20 }) tableData.value = rows } finally { loading.value = false } }

但如果你要在这个表格里做行内编辑,保存时只更新某一行:

const saveRow = async ({ row }) => { await updateUser(row) xTable.value.updateData({ row, field: 'status', value: 'saved' }) }

这种情况下你用实例方法会更精准,不用重建整个数组。

不过这不是说两条路不能混用,而是要明白每条路各自的重新渲染范围和状态影响。接下来我展开讲加载数据的几种具体方式。

2. 加载数据的几种方式:从直观到进阶

2.1 声明式绑定:直接给 :data 赋值

最直观的加载方式当然是直接给表格的data属性绑定一个数组,这也是大多数人刚上手 vxe-table 时最先接触的。它最大的优点就是符合 Vue 的响应式直觉:你把数据交给组件,组件负责渲染,后续只要tableData.value = newList,表格就会刷新。

这种方式的完整代码大概长这样:

<template> <vxe-table :data="tableData" :loading="loading"> <vxe-column field="id" title="ID" width="80"></vxe-column> <vxe-column field="name" title="姓名" width="150"></vxe-column> <vxe-column field="age" title="年龄" width="120"></vxe-column> </vxe-table> </template> <script setup> import { ref, onMounted } from 'vue' const tableData = ref([]) const loading = ref(false) const queryList = async () => { loading.value = true try { const data = await fetch('/api/user/list').then(res => res.json()) tableData.value = data.rows || [] } finally { loading.value = false } } onMounted(queryList) </script>

这里有一个容易被忽略的点:vxe-table的data支持v-model双向绑定,在 4.x 版本里你可以直接写v-model="tableData",效果等同于:data=""加@update:data。但实战中我更倾向于只写:data,因为双向绑定虽然在数据回写时方便,却容易让外部数组被表格内部操作"悄悄修改",排查问题时多一层不确定。

声明式绑定适合以下场景:

  • 表格数据量不大,基本在一页内展示完。
  • 数据流简单,查询结果直接覆盖旧数据。
  • 没有复杂的行状态维护需求,刷新后即使选中态丢失也能接受。

缺点也很明显:每次全量替换数组,虚拟滚动场景下卡顿感较弱,但如果你对行状态有强依赖,比如翻页后要保留之前的勾选,直接替换tableData无法保留这些状态,需要自己存储并回填,这活儿就麻烦了。

另外注意一点,直接替换数组时,如果新的数组引用被多个地方共享,要小心后续的push、splice操作污染了另一个页面的数据。我习惯在接口返回后做一次浅拷贝再赋值:

tableData.value = rows.map(item => ({ ...item }))

这样至少能避免同一对象引用被多处改动。

2.2 命令式加载:通过 ref 调用 loadData 方法

loadData是 vxe-table 表格实例上的方法,官方原意是"加载数据到表格"。它和直接改:data的最大区别在于:loadData会尽可能保留表格的现有状态。

先说用法。给表格加ref,然后在异步请求回来后调用:

<template> <vxe-table ref="xTable" :data="tableData"></vxe-table> </template> <script setup> import { ref } from 'vue' const xTable = ref() const tableData = ref([]) const fetchData = async () => { const { rows } = await getData() xTable.value.loadData(rows) } </script>

如果你用<script setup>,记得在模板里给组件加上ref="xTable",然后在onMounted之后调用,否则xTable.value可能是null。异步请求通常在onMounted时发起,等响应回来时实例已经挂载,所以问题不大;但如果你在组件创建后立即同步调用,那就很可能会拿不到实例。

loadData做了什么事?它会把传入的数据替换为表格当前的数据源,同时尝试保留:

  • 展开行状态
  • 勾选状态(如果有checkbox-config)
  • 当前高亮行
  • 排序状态

原理大概是这样:表格记录内部会对比新旧数据,把能匹配的行的状态迁移过来,匹配不上的行状态就丢弃。正因如此,它非常适合"刷新数据但不想丢掉用户操作痕迹"的场景。

不过要注意:loadData对"状态保留"不是绝对的。如果你的主键列没有配置row-key,或者新旧数据在比对时找不到对应行,状态一样会丢。所以想在翻页后保留勾选,正确做法是给vxe-table配置row-key和checkbox-config的preserve属性:

<vxe-table ref="xTable" row-key="id" :data="tableData" :checkbox-config="{ highlight: true, checkMethod: () => true }" >

没有row-key之前,我吃过一两次亏,翻页回来勾选全没了,排查半天才发现是这个配置没写。

2.3 重载数据:reloadData 与 loadData 的区别

reloadData在命名上就和loadData很像,官方定位于"重载数据"。区别在于它会重新初始化表格的很多内部状态:排序、筛选、展开行、选中行、高亮行,全部重置为初始值,相当于你第一次拿到这个数据源时看到的干净表格。

换个说法:loadData是"尽量保持现场刷新数据",reloadData是"清空现场重新来过"。

它们在代码层面只是换一个方法名:

// 保留状态 xTable.value.loadData(rows) // 重置所有状态 xTable.value.reloadData(rows)

我实际项目里的取舍习惯是这样:查询按钮点击后用reloadData,因为用户主动查询时通常希望看到一个全新的、符合筛选条件的结果;而定时轮询刷新数据、或者静默同步最新记录时用loadData,避免把用户正在操作的表格状态打断。

另外有一点非常实用:reloadData在某些版本里能顺带重置表格内部的排序和筛选条件,但如果你想把外部绑定的排序参数也重置,记得同时清空自己的查询参数。

2.4 全局加载:full-data 与 VXETable.setConfig

这一节要讲的可能是大多数人没留意过的隐藏玩法:vxe-table 还支持通过全局配置加载数据。怎么做?先用VXETable.setConfig全局设置一份数据,然后表格上声明full-data:

import VXETable from 'vxe-table' VXETable.setConfig({ table: { data: [ { id: 1, name: '张三', age: 20 }, { id: 2, name: '李四', age: 26 } ] } })

模板里:

<vxe-table full-data> <vxe-column field="id" title="ID"></vxe-column> <vxe-column field="name" title="姓名"></vxe-column> </vxe-table>

加了full-data之后,表格会忽略局部的data属性,直接读取全局配置里的数据源。这种方式的实用场景非常有限,我推测官方设计它主要是为了满足一些特殊项目里"全局统一数据源、多个表格共享同一份数据"的需求,比如多个表格同时展示一份配置数据,统一在某个入口刷新后所有表格自动联动。

但说实话,普通业务项目我不太推荐用。全局数据意味着任何一处调用setConfig都会影响所有声明了full-data的表格,调试时很难定位是哪段代码改了全局数据。而且它与团队协作时的数据流设计相悖,别人看你的模板时很难一眼看出数据从哪里来。

如果你只是想要一个"多表格共享数据"的效果,更常规的做法是抽一个公共 store,比如 Pinia,维护一份commonData,每个表格的:data都从 store 里取。这样数据来源明确,也方便在各处手动刷新。

3. 更新数据的完整用法:方法选型与语义

3.1 setData / loadData / reloadData 三兄弟的区别

更新数据这一块,我经常在社区看到这种问题:"我把数据从接口拿回来后,该用setData还是loadData?"这说明官方文档虽然 API 写得很清楚,但缺少场景化说明。

简单梳理一下:

  • setData(records):直接设置表格数据,不带有明显状态保留语义,适合全量覆盖。
  • loadData(records):加载数据,尽量保留表格状态。
  • reloadData(records):重载数据,清空表格状态。

理论上看,setData和loadData在大部分场景下最终效果差不多,都是把新数据替换旧数据。但从 vxe-table 内部实现和实测行为来看,loadData更侧重于"保留现场",setData更侧重于"直接覆盖"。

我自己的使用规则是:

方法适用场景行状态影响
setData初始化表格、全量覆盖数据不做额外状态保留
loadData刷新数据但想保留展开、勾选等状态尽量保留,依赖row-key
reloadData查询、筛选、重置后展示全新结果重置所有状态

拿真实需求举例。我的一个后台项目里有张订单表格,用户会勾选几行订单,点"批量退款"按钮,提交接口成功后我需要刷新表格。这时候如果直接reloadData,用户勾选的订单虽然在刷新后已经从列表移除,但由于状态重置,其他未勾选订单的勾选状态、当前展开的明细行也没了,体验很差。改成loadData后,未勾选订单的勾选状态能尽量保留,展开行也还在。

不过注意,loadData保留状态依赖row-key能稳定匹配新旧数据。如果接口返回的数据里没有稳定唯一键,建议在接口层就补齐,比如把数据库主键或业务唯一单号返回来。

另外,setData、loadData、reloadData传入的参数都是普通数组,不需要传响应式代理对象。如果你从 Pinia 或 Vuex 里直接取数组传进去,vxe-table 内部会做自己的数据接管,外部 store 数组不会因为表格内部编辑而自动同步。想要同步的话,要么监听update:data事件手动更新 store,要么保持外部数组是唯一数据源,不走这些方法。

3.2 单行更新:setRow 与 updateData

很多新手对"更新某一行"的第一反应是:重新查一遍列表,然后tableData.value = rows。这样做的确能刷新,但代价很大:接口请求一次、列表闪烁一下、可能在表格组件里丢状态。如果是编辑弹窗保存后只想更新那几行,完全没必要重查整表。

vxe-table 提供了两组单行粒度更新方法:setRow和updateData。

先说setRow,它的签名大致是:

setRow(row, field, value)

表示把某一行对象的field字段更新为value。这里的row是表格里行的引用,也就是你可以从@edit-closed事件回调里拿到的row,也可以从表格数据数组里找出来。

const updateAge = ({ row }) => { xTable.value.setRow(row, 'age', 30) }

updateData则更灵活,官方文档里支持两种入参形式。一种是更新整个行的数据字段,另一种是更新单个单元格:

// 更新整行多个字段 xTable.value.updateData({ row, fields: ['name', 'age', 'status'] }) // 更新单元格 xTable.value.updateData({ row, field: 'status', value: 'success' })

注意updateData传入的是一个对象,不是直接传值。这个细节很容易踩坑,我看到过有人写updateData(row, 'name', '张三'),结果报错也没反应,就是因为把方法签名和setRow混了。

另外,setRow和updateData都只是把新值更新到表格内部数据,不会自动触发接口提交。真正要保存到后端,你需要在编辑事件里自己调接口,接口成功后可以再用这些方法把最新状态同步回表格。

3.3 新增、删除与清空:insert / remove / clearData

除了修改已有数据,表格更新数据的高频操作还有新增行、删除行、清空数据。

insert方法的典型用法是把新记录插到表格数据末尾:

const addRow = () => { const newRow = { id: Date.now(), name: '', age: null } xTable.value.insert(newRow) }

如果你想插入到指定行后面,可以传第二个参数,表示插在哪一行附近:

xTable.value.insert(newRow, currentRow)

删除行用remove,传入行对象或者行对象数组:

const deleteRows = (rows) => { xTable.value.remove(rows) }

如果你用了复选框,并且通过checkbox-config开启了勾选功能,可以用removeCheckboxRow快速删除所有勾选行:

xTable.value.removeCheckboxRow()

这个方法的语义比你自己遍历勾选行再filter干净得多,尤其在大数据量勾选场景下,它能省掉大量手动维护选中集合的代码。

清空数据用clearData,它会清掉表格内部所有记录,同时重置行状态。注意clearData和把外部数组赋空数组tableData.value = []并不完全等价,前者会连表格内部的一些状态一起重置,后者只改变绑定数据,某些内部状态可能残留。如果你在切换业务模块时需要完全重置表格,优先用clearData。

4. 真实项目里的加载与更新实践(含避坑)

4.1 异步加载的 loading 与竞态处理

异步加载数据是后台管理系统的基本操作,但有两件事经常被忽略:loading 状态的收尾,和请求竞态。

loading 状态我见过很多人只写开头不写结尾,结果接口报错后表格一直转圈。正确姿势是无论成功失败都要把 loading 关掉,用try...finally最稳:

const queryList = async () => { loading.value = true try { const { rows } = await fetchList(params) xTable.value.loadData(rows) } finally { loading.value = false } }

竞态问题更隐蔽。用户快速切换分页页码、或者连续点击两次查询按钮时,较晚发出的请求可能先返回,较早发出的请求后返回,最终表格展示的是过期数据。我早期在分页表格上遇到过几次:点第 3 页,页面先加载出来了,再点第 2 页,结果表格数据突然变回第 1 页的内容,排查了一会儿才发现是竞态。

解决方案也不复杂,用请求序号做标记即可:

let querySeq = 0 const queryList = async () => { const currentSeq = ++querySeq loading.value = true try { const { rows } = await fetchList(params) if (currentSeq === querySeq) { xTable.value.loadData(rows) } } finally { if (currentSeq === querySeq) { loading.value = false } } }

做了这个保护后,哪怕旧请求后返回,也不会覆盖更新后的数据。

4.2 分页场景下的数据加载顺序

分页表格里,通常表格和分页组件是一起使用的,vxe-table 官方有配套的vxe-pager。经典的加载流程是:

  1. 分页参数变化。
  2. 根据currentPage和pageSize请求接口。
  3. 拿到total后设置分页组件总条数。
  4. 表格加载当前页数据。

针对这个流程,我建议把"设置总条数"和"给表格喂数据"放在同一次数据更新里做,避免分页组件和数据源不一致导致用户翻页时看到空的表格。

const pageParams = reactive({ pageNo: 1, pageSize: 20, total: 0 }) const queryList = async () => { loading.value = true try { const { rows, total } = await fetchList({ pageNo: pageParams.pageNo, pageSize: pageParams.pageSize }) pageParams.total = total pagerRef.value.setTotal(total) xTable.value.reloadData(rows) } finally { loading.value = false } }

这里我用的是reloadData而不是loadData,因为用户主动翻页、筛选后,通常希望看到一个全新的数据结果,旧的行状态在语义上不合时宜。不过如果你们产品要求翻页保留勾选,那就得改用loadData,并且加好row-key和checkbox-config的保留配置。

4.3 直接操作外部数组可能踩的雷

声明式绑定的确方便,但有一个常见误区:你以为:data绑定的数组和表格内部数据是同一个引用。其实不完全是。当你通过表格内置功能修改数据(比如编辑单元格、勾选删除)时,vxe-table 会把结果通过update:data事件同步到外部数组;但如果你直接给tableData.value赋一个新的数组,表格内部的行状态会基于上一次的数据结构重新计算,可能导致展开、选中等状态丢失。

更隐蔽的问题是,如果你不写@update:data,而外部数组又被 fork 了一份传给别的地方,表格内部的变化不会自动更新到其他引用。举个我踩过的例子:

const rawList = await fetchList() const copyList = rawList.rows.map(item => ({ ...item })) tableData.value = copyList

表格渲染用copyList,但我另一条业务逻辑操作的却是rawList.rows,结果表格里改完数据,rawList里的对象还是旧值。后来我统一了数据流转,明确"接口返回后只保留一份数据源,所有操作都基于这一份",这类问题才消失。

所以我的建议是:如果表格要参与编辑、删除这类内部操作,尽量用 ref + 实例方法完成,外部数组只负责初始渲染和查询结果的展示。不要在多个地方持有数据引用,否则"改不动""不同步"的问题迟早找上门。

4.4 大数据量下的加载优化建议

vxe-table 能扛大数据量,但这不代表你可以无脑往data里塞几万条记录。数据量大了以后,加载和更新的差距会变得明显。举个例子,接口一次性返回 2 万行,你直接用tableData.value = rows赋值,表格整体渲染一次的成本并不算低;如果这时候再频繁调用reloadData,会造成明显的卡顿和闪烁。

我处理大数据量的经验是:

  1. 优先开启虚拟滚动。height或max-height加上scroll-y的虚拟滚动配置,让表格只渲染可视区域的数据,而不是一次性把所有行渲染进 DOM。
<vxe-table :data="tableData" height="600" :scroll-y="{ enabled: true }" ></vxe-table>
  1. 如果做大范围的单元格更新,尽量用loadData一次性替换,而不是循环调setRow改几百行。虽然setRow是局部更新,但循环触发大量更新同样会让表格频繁重绘。

  2. 数据源尽量是纯对象数组,不要在数组里挂函数、复杂嵌套对象,否则 vxe-table 内部做状态记录和对比时的损耗会明显上升。

  3. 后端分页永远比前端一次性加载更稳。哪怕是 vxe-table 这种性能不错的表格,数据量到十万行级别时,建议不要硬扛前端渲染,该分页还是分页,或者用懒加载加上滚动到底自动请求下一页的方案。

我在一个报表项目里把所有明细数据一次性灌到前端再做列统计,结果浏览器直接卡到无响应。后来改成部分明细 + 后端聚合统计,体验和性能立刻正常了。所以数据加载策略一定要跟着业务量级走,不要迷信组件性能。

4.5 配合 vxe-grid 与 proxy-config 的进阶方案

最后提一个很多人会忽略的进阶玩法:vxe-table 的完整版还提供了vxe-grid组件,它能配合proxy-config实现代理模式的数据加载。什么意思?就是你不必手写"请求接口 → 赋值给 data → 手动管理 loading"这一整套流程,只需要给 grid 配置好查询接口和参数,组件自己会处理加载、分页、排序、筛选。

简单示例:

<template> <vxe-grid v-bind="gridOptions"></vxe-grid> </template> <script setup> import { reactive } from 'vue' const gridOptions = reactive({ proxyConfig: { query: async ({ page, sorts, filters }) => { const { rows, total } = await fetchList({ pageNo: page.currentPage, pageSize: page.pageSize, sorts, filters }) return { result: rows, total } } }, columns: [ { field: 'id', title: 'ID' }, { field: 'name', title: '姓名' } ] }) </script>

配置了proxyConfig.query后,vxe-grid会在初始化、翻页、排序、筛选时自动调用你的查询函数,并把返回值塞进表格。你不需要手动维护 loading,组件内部会处理。这是数据加载方式的"终极形态",适合中大型项目里数据流高度统一的场景。当然它也有学习成本,如果你刚接触 vxe-table,建议还是先从简单的:data和loadData上手,等熟悉了再往 proxy 模式迁。

写到这里,我在实际项目里最常见的感受是:vxe-table 不是不好用,而是它的数据管理方式比普通表格组件多了一层"内部数据与外部数据"的抽象,一旦你适应了"能用声明式就用声明式,需要精细控制时果断切实例方法"这个思路,大部分加载和更新问题都不是问题。另外,不管用哪种方式,一定先明确 row-key 的配置,它是状态保留和数据匹配的基石,也是我排障时最先检查的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询