本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Vue.js是构建用户界面的主流JavaScript框架,广泛应用于单页应用开发。本资源“vue.js动态表格增加删除修改代码.zip”提供了一个完整的动态表格实现案例,涵盖数据的添加、删除、编辑等交互功能。通过index.html作为入口页面,结合Vue实例管理表格数据与行为逻辑,利用v-for、v-on等指令实现数据绑定与事件响应。js文件夹包含核心业务逻辑,说明文件则帮助开发者快速理解项目结构与运行方式。该示例深入展示了Vue.js的响应式机制、组件化设计及前端交互处理,适合初学者学习与实际项目参考。

Vue.js 动态表格开发实战:从基础语法到组件化工程落地

你有没有遇到过这样的场景?产品拿着一张 Excel 表格走过来:“我们要把这个做成网页版,支持增删改查,最好还能排序筛选。” 你心里一紧——这要是用原生 JavaScript 写,光是 DOM 操作就得写几百行,还得手动处理数据同步、状态管理……简直是噩梦 🫠。

但如果你用的是 Vue.js ,事情就变得不一样了。它就像一个聪明的“管家”,你说:“我这儿有份数据,帮我显示成表格。” 它二话不说,自动生成页面;你再改个数据,它立刻更新视图,根本不用你动手操作 DOM。这才是现代前端该有的样子 ✨!

今天我们就来一起打造一个 真正可用的动态表格系统 ,不玩虚的,直接上硬核实战。我们会从最基础的 v-for 开始,一步步深入到响应式原理、用户交互设计,最后完成模块化封装,让你写出的代码不仅跑得快,还经得起团队协作和后期维护的考验 💪。

准备好了吗?Let’s go!🚀


数据驱动的魔法:Vue 如何让表格“活”起来?

在传统开发中,我们总是“先有界面,再填数据”。比如你要展示一个用户列表,就得先写好 HTML 结构,然后通过 document.getElementById() 找到对应元素,再一个个设置内容……一旦数据变了,你还得重新走一遍这个流程。

而 Vue 的思路完全不同——它是 “数据即视图” 。你只需要告诉它:“这是我的数据”,剩下的渲染、更新,全都交给框架自动完成。这种模式叫做 响应式编程(Reactive Programming)

想象一下,你的数据是一个湖面,而 UI 是倒映其中的风景。当你往湖里扔一块石头(修改数据),水面泛起涟漪(触发更新),倒影自然也会跟着波动(视图刷新)。整个过程无需人工干预,一切水到渠成 🌊。

那么问题来了:Vue 到底是怎么做到这一点的呢?答案藏在它的核心机制里—— 响应式系统 + 虚拟 DOM + 模板编译

不过别担心,我们不会一开始就钻进源码海洋里淹死自己 😅。咱们先从看得见摸得着的地方入手——怎么用 v-for 把数组变成表格?


v-for:动态渲染的基石,但你真的会用吗?

说到循环渲染, v-for 是每个 Vue 开发者最早接触的指令之一。语法也很简单:

<tr v-for="user in users" :key="user.id">
  <td>{{ user.name }}</td>
  <td>{{ user.age }}</td>
</tr>

看起来很 straightforward 对吧?但这里有个关键细节很多人忽略了—— :key

key 不只是个属性,它是 Vue 的“身份识别卡”

你可以把每个 DOM 元素想象成一个人,而 key 就是他的身份证号。如果没有身份证,警察叔叔只能靠外貌判断谁是谁。可万一两个人长得一样呢?或者其中一人整容了?那岂不是乱套了?

Vue 在更新列表时也面临同样的问题。默认情况下,它采用“就地复用”策略:相同位置的元素会被认为是同一个,直接复用并修改内容。这虽然性能高,但在插入、删除或排序时就会出问题。

举个例子:

// 原数组
users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

// 插入新用户
users.unshift({ id: 3, name: 'Charlie' })

这时候如果 key 是索引( :key="index" ),会发生什么?

旧位置 新位置 实际变化
index=0 → Alice index=0 → Charlie Vue 认为 Alice 还在这儿,只是改名叫 Charlie 了
index=1 → Bob index=1 → Alice Vue 认为 Bob 改名叫 Alice 了

结果就是: 名字对上了,但其他状态全乱了! 如果某一行正在编辑、展开详情、加载动画……这些状态都会错位!

所以官方文档反复强调: 请使用唯一且稳定的 key,比如数据库 ID

🔑 最佳实践 :永远不要用 index 作为 key ,除非你能保证数组永不增删改顺序!

<!-- ✅ 推荐 -->
<tr v-for="user in users" :key="user.id">

<!-- ❌ 避免 -->
<tr v-for="(user, index) in users" :key="index">

下面这张流程图清晰展示了 Vue 在有无 key 时的不同行为路径:

graph TD
    A[开始更新列表] --> B{是否存在key?}
    B -- 否 --> C[按位置复用DOM]
    B -- 是 --> D[对比新旧key集合]
    D --> E[识别新增、移动、删除节点]
    E --> F[执行最小化DOM操作]
    F --> G[完成视图更新]

看到了吗?有了 key ,Vue 才能精准追踪每个元素的身份,做出最优更新决策。否则只能“盲人摸象”,靠猜!


构建第一个动态表格:从静态数据到配置化列

现在我们来动手实现一个完整的表格组件。假设后端返回的数据长这样:

[
  { "id": 1, "name": "张三", "age": 28, "status": "active" },
  { "id": 2, "name": "李四", "age": 32, "status": "inactive" }
]

我们的目标是把它渲染成带样式的 HTML 表格,并支持未来扩展功能。

基础版本:硬编码列结构

最简单的做法就是直接写死列名:

<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>姓名</th>
      <th>年龄</th>
      <th>状态</th>
    </tr>
  </thead>
  <tbody>
    <tr v-for="row in tableData" :key="row.id">
      <td>{{ row.id }}</td>
      <td>{{ row.name }}</td>
      <td>{{ row.age }}</td>
      <td :class="{ active: row.status === 'active', inactive: row.status === 'inactive' }">
        {{ row.status }}
      </td>
    </tr>
  </tbody>
</table>

对应的 Vue 实例:

new Vue({
  el: '#app',
  data() {
    return {
      tableData: [
        { id: 1, name: '张三', age: 28, status: 'active' },
        { id: 2, name: '李四', age: 32, status: 'inactive' }
      ]
    }
  }
})

这段代码已经可以正常工作了 👏。但我们很快就会发现一个问题: 如果另一个页面也需要表格,但字段不同怎么办?复制粘贴?改来改去?

显然这不是长久之计。我们需要更灵活的设计。

升级版:列配置驱动,打造通用表格

解决办法很简单—— 把列定义抽出来,作为配置项传进去

data() {
  return {
    columns: [
      { field: 'id', label: 'ID' },
      { field: 'name', label: '姓名' },
      { field: 'age', label: '年龄' },
      { field: 'status', label: '状态' }
    ],
    tableData: [/* ... */]
  }
}

模板中使用双重 v-for 实现动态列渲染:

<tr v-for="row in tableData" :key="row.id">
  <td v-for="col in columns" :key="col.field">
    {{ row[col.field] }}
  </td>
</tr>

哇哦!就这么几行代码,我们就完成了一个 可复用的基础表格框架 🎉。以后只要换个 columns 配置,就能展示订单、商品、日志等各种数据,完全不用动模板逻辑!

而且还可以继续增强:

  • 给列加 width 控制宽度
  • sortable: true 支持点击排序
  • formatter 函数格式化显示(如时间戳转日期)
  • hidden: true 控制是否显示

这些都可以通过配置轻松实现,真正的“一次编写,处处使用”🌟。


性能优化实战:当表格有上千行时该怎么办?

理想很丰满,现实很骨感。当你兴奋地把一万条数据丢进表格时,浏览器突然卡住,风扇狂转……😱

别慌,这是典型的性能瓶颈。我们来分析下原因,并给出解决方案。

为什么大表格会卡?三大元凶浮出水面

根据经验统计,大型表格的性能问题主要来自以下三个方面:

pie
    title 渲染性能影响因素占比
    “缺少key” : 35
    “频繁计算” : 25
    “过多DOM节点” : 30
    “其他” : 10
1. 缺少 key —— 每次都重建 DOM

前面说过,没有 key 会导致 Vue 无法智能 diff,每次更新都可能全量替换子节点。即使只改了一行数据,也可能导致所有 <tr> 被重新创建,开销巨大!

对策 :确保每行都有唯一稳定 key

2. 频繁计算 —— 每次渲染都在做重复劳动

比如你在模板里写了这样一个函数调用:

<td>{{ formatDate(row.createdAt) }}</td>

你以为只是显示个时间?错!每次视图更新(哪怕别的地方变了),这个函数都会被执行一遍。如果有 1000 行,那就是 1000 次函数调用!

对策 :使用 computed 属性缓存计算结果。

computed: {
  formattedTableData() {
    return this.rawData.map(item => ({
      ...item,
      formattedDate: formatDate(item.createdAt)
    }))
  }
}

这样只有当 rawData 真正变化时才会重新计算,极大减少 CPU 占用。

3. 过多 DOM 节点 —— 浏览器撑不住了

HTML 页面的本质是一棵巨大的 DOM 树。研究表明,当节点数超过 1000 时,大多数浏览器就开始出现明显卡顿。而一个千行表格,光 <td> 就可能有几千个……

终极方案 :虚拟滚动(Virtual Scrolling)

只渲染当前可视区域内的行,其余用空白占位。用户滚动时动态切换内容,视觉上毫无破绽,内存占用却大幅降低。

实现原理如下:

<div class="virtual-scroll-container" ref="container" @scroll="handleScroll">
  <div 
    v-for="row in visibleRows" 
    :key="row.id" 
    :style="{ position: 'absolute', top: row.offsetTop + 'px' }"
  >
    {{ row.content }}
  </div>
</div>

配合 JS 计算可视范围:

methods: {
  updateVisibleRows() {
    const scrollTop = this.$refs.container.scrollTop;
    const viewportHeight = this.$refs.container.clientHeight;
    this.visibleRows = this.allRows.filter(row => 
      row.offsetTop > scrollTop - 100 && 
      row.offsetTop < scrollTop + viewportHeight + 100
    );
  }
}

虽然实现略复杂,但市面上已有成熟库如 vue-virtual-scroller ,拿来即用,强烈推荐用于大数据场景。

优化手段 适用场景 性能收益
添加 key 所有 v-for 场景
使用 computed 数据需格式化 中高
虚拟滚动 >1000 行数据 极高
函数防抖/节流 搜索/排序触发

记住一句话: 小数据靠规范,大数据靠架构 。合理的优化能让用户体验从“卡爆”变成“丝滑流畅”✨。


用户交互来了!让表格真正“动”起来

光看不动的表格叫“报表”,能 CRUD 的才叫“管理系统”。接下来我们就给表格加上灵魂——用户交互能力。

添加功能:表单显隐控制的艺术

最常见的需求是在表格上方加个“添加”按钮,点击后弹出输入表单。

<button @click="showForm = !showForm">
  {{ showForm ? '取消添加' : '添加新条目' }}
</button>

<form v-if="showForm" @submit.prevent="addItem">
  <input v-model="newItem.name" placeholder="姓名" required />
  <input v-model.number="newItem.age" type="number" placeholder="年龄" required />
  <button type="submit">保存</button>
  <button type="button" @click="showForm = false">取消</button>
</form>
data() {
  return {
    showForm: false,
    newItem: { name: '', age: null },
    items: []
  };
},
methods: {
  addItem() {
    if (!this.newItem.name || !this.newItem.age) {
      alert('请填写完整信息!');
      return;
    }

    const item = {
      id: Date.now(), // 简易ID生成
      ...this.newItem
    };

    this.items.push(item);
    this.newItem = { name: '', age: null }; // 重置
    this.showForm = false; // 关闭表单
  }
}

这里有几个细节值得注意:

  • @submit.prevent 阻止默认提交行为,防止页面刷新;
  • v-model.number 自动将输入转为数字类型;
  • 提交前做空值校验,提升用户体验;
  • 成功后清空表单并隐藏,准备下次输入。

整个过程体现了 Vue 的核心思想: UI = f(data) 。只要数据变了,视图自然跟着变,无需手动操作任何 DOM。

下面是这个功能的状态流转图:

stateDiagram-v2
  [*] --> 隐藏状态
  隐藏状态 --> 显示状态: 点击“添加新条目”
  显示状态 --> 隐藏状态: 点击“取消”或提交成功
  显示状态 --> 隐藏状态: 提交表单并保存数据

是不是有种“状态机”的感觉?其实很多复杂的交互都可以用这种方式建模,逻辑清晰又不容易出错。


删除功能:小心事件冒泡的“连环爆炸”

删除按钮通常放在每一行的操作列里:

<button @click="confirmDelete(item)">删除</button>

但在嵌套结构中,要特别注意 事件冒泡(Event Bubbling)

假设你在 <tr> 上也绑定了点击事件(比如点击整行进入编辑模式),那么当你点击“删除”按钮时,事件会先在按钮上触发,然后逐层向上传播到 <tr> ,导致意外激活编辑功能!

后果可能是:你本想删一条数据,结果变成了“删完马上进入编辑”😱。

解决方案有两个:

  1. 在方法中手动阻止:
    js confirmDelete(item, event) { event.stopPropagation(); // 阻止向上冒泡 // ... }

  2. 使用 .stop 修饰符(推荐):
    html <button @click.stop="deleteItem(item.id)">删除</button>

.stop 是 Vue 提供的便捷语法糖,等价于调用 event.stopPropagation() ,写起来更简洁。

另外,删除前最好加个确认框:

confirmDelete(id) {
  const confirmed = window.confirm('确定要删除吗?');
  if (confirmed) {
    this.deleteItem(id);
  }
}

虽然 window.confirm 比较原始,但对于中小型项目足够用了。追求美观可以用 Element Plus 或 Ant Design Vue 的 Modal 组件替代。

下面是删除操作的完整流程:

graph TD
  A[用户点击删除按钮] --> B{是否确认?}
  B -->|否| C[取消操作]
  B -->|是| D[执行deleteItem()]
  D --> E[从items中移除该项]
  E --> F[视图自动更新]

干净利落,一气呵成!


编辑功能:如何安全地进行内联编辑?

真正的挑战来了—— 行内编辑(Inline Editing) 。这是企业级后台系统的标配功能,但实现起来容易踩坑。

常见做法是给每行加个 isEditing 字段,控制是否显示输入框:

<td>
  <span v-if="!item.isEditing">{{ item.name }}</span>
  <input v-else v-model="item.editName" />
</td>
methods: {
  startEdit(item) {
    this.$set(item, 'isEditing', true);
    this.$set(item, 'editName', item.name);
  },
  saveEdit(item) {
    item.name = item.editName;
    this.$set(item, 'isEditing', false);
  },
  cancelEdit(item) {
    this.$set(item, 'isEditing', false);
  }
}

注意这里用了 this.$set ,而不是直接赋值:

item.isEditing = true; // ❌ 可能无效!

为什么?因为 Vue 2 使用 Object.defineProperty 劫持属性访问,只能侦测已存在的属性。如果对象原本没有 isEditing 字段,直接添加是不会触发响应式的!

所以必须用 $set 手动通知 Vue:“嘿,这个新属性也要监听哦!”

到了 Vue 3,由于改用 Proxy ,这个问题基本解决了,但为了兼容性,建议仍使用 $set

还有一个重要设计: 编辑期间不要直接修改原字段

我们用了 editName 作为临时字段,保存用户输入。这样即使用户中途取消,也不会污染原始数据。这就是所谓的“数据隔离”原则。

下面是编辑功能的生命周期图:

flowchart LR
  Start[点击编辑按钮] --> SetEditing{设置 isEditing=true}
  SetEditing --> CloneData[复制原始数据到临时字段]
  CloneData --> RenderInput[渲染输入框]
  RenderInput --> WaitUser[等待用户输入]
  WaitUser --> SaveOrCancel{点击保存 or 取消?}
  SaveOrCancel -->|保存| UpdateModel[更新原始数据]
  SaveOrCancel -->|取消| Restore[恢复原值]
  UpdateModel --> ExitEdit[退出编辑模式]
  Restore --> ExitEdit
  ExitEdit --> RenderText[重新渲染文本]

看到没?每一个环节都要考虑“失败回滚”的可能性。好的程序不仅要处理成功路径,更要优雅应对各种异常情况。


响应式原理揭秘:Vue 是如何监听数据变化的?

讲了这么多实战技巧,你是不是也好奇:Vue 底层到底是怎么实现响应式的?

我们以 Vue 2 为例,核心机制有三个关键词: Observer、Watcher、Dep

Observer:劫持数据访问

Vue 在初始化时会递归遍历 data 中的所有属性,使用 Object.defineProperty 改造它们的 getter/setter:

function defineReactive(obj, key, val) {
  const dep = new Dep(); // 每个属性有自己的依赖收集器

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) {
        dep.depend(); // 收集依赖
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      dep.notify(); // 通知更新
    }
  });
}

这样一来,任何读取或修改操作都会被拦截。

Watcher:连接数据与视图的桥梁

当模板被解析时,Vue 会创建一个 Watcher 实例,并将其设为 Dep.target 。此时访问任何响应式数据,都会触发其 getter,从而被 dep 收集为依赖。

比如:

<p>{{ user.name }}</p>

解析过程中会访问 user.name ,于是对应的 dep 就记住了:“这个 p 标签依赖我”。

Dep:依赖管理中心

每个响应式字段都有一个 Dep 实例,维护着所有依赖它的 Watcher 列表。

当数据改变时,setter 触发 dep.notify() ,通知所有 Watcher 更新,进而触发视图重新渲染。

整个流程如下:

graph LR
    A[数据变化] --> B(v-bind触发)
    B --> C{绑定类型}
    C --> D[class绑定]
    C --> E[style绑定]
    D --> F[更新className]
    E --> G[更新style属性]
    F & G --> H[浏览器重绘]

至于数组,Vue 重写了 push/pop/splice 等七个变异方法,在内部手动触发通知,确保视图同步更新。

但也因此带来一个限制: 不能通过索引直接设置数组项

this.tableData[0] = { ... }; // ❌ 不触发更新

解决办法还是 $set

this.$set(this.tableData, 0, newItem); // ✅ 强制响应式
操作方式 是否触发更新 建议替代方案
arr.push(item) 直接使用
arr[i] = newValue this.$set(arr, i, v)
obj.newProp = value this.$set(obj, 'k', v)

理解这些底层机制,不仅能帮你避开坑,还能写出更高效的代码。毕竟,“知其然更知其所以然”才是高手之道 🔍。


组件化重构:把表格拆成独立模块,走向工程化

随着功能越来越多,所有逻辑挤在一个文件里会越来越难维护。是时候进行 组件化拆分 了!

封装 TableComponent:职责分离的第一步

我们创建一个 TableComponent.vue 文件,专门负责表格渲染:

<template>
  <table class="data-table">
    <thead>
      <tr>
        <th v-for="col in columns" :key="col.field">{{ col.label }}</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="row.id">
        <td v-for="col in columns" :key="col.field">
          {{ row[col.field] }}
        </td>
        <td>
          <button @click="$emit('edit', row)">编辑</button>
          <button @click.stop="$emit('delete', row.id)">删除</button>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  name: 'TableComponent',
  props: {
    data: Array,
    columns: {
      type: Array,
      default: () => [
        { field: 'name', label: '姓名' },
        { field: 'age', label: '年龄' }
      ]
    }
  }
}
</script>

父组件只需引入并传递数据:

<template>
  <div>
    <TableComponent 
      :data="tableData" 
      @edit="handleEdit" 
      @delete="handleDelete" 
    />
  </div>
</template>

<script>
import TableComponent from './components/TableComponent.vue'

export default {
  components: { TableComponent },
  data() { /* ... */ },
  methods: {
    handleEdit(row) { /* ... */ },
    handleDelete(id) {
      this.tableData = this.tableData.filter(item => item.id !== id)
    }
  }
}
</script>

这就形成了经典的 “数据向下,事件向上” 通信模式,清晰划分了父子组件的职责边界。


插槽(Slot)加持:让组件更灵活

有时候默认的操作按钮不够用,比如还要加“导出”、“查看详情”等功能。这时候 slot 就派上用场了。

改造子组件:

<td>
  <slot name="actions" :row="row">
    <!-- 默认内容 -->
    <button @click="$emit('edit', row)">编辑</button>
    <button @click.stop="$emit('delete', row.id)">删除</button>
  </slot>
</td>

父组件自定义内容:

<TableComponent :data="tableData">
  <template #actions="{ row }">
    <button @click="handleDetail(row)">详情</button>
    <button @click="handleExport(row)">导出</button>
  </template>
</TableComponent>

这样一来,子组件保持通用性,父组件又能自由扩展,两全其美!


业务逻辑抽离:告别臃肿的 methods

methods 越来越长时,我们可以把这些函数移到独立的 JS 文件中,形成服务层:

// services/tableService.js
export function addNewItem(list, item) {
  return [...list, { ...item, id: Date.now() }];
}

export function removeItemById(list, id) {
  return list.filter(item => item.id !== id);
}

export function updateItemById(list, updatedItem) {
  return list.map(item => 
    item.id === updatedItem.id ? { ...updatedItem } : item
  );
}

组件中导入使用:

import { removeItemById } from '@/services/tableService'

export default {
  methods: {
    handleDelete(id) {
      this.tableData = removeItemById(this.tableData, id)
    }
  }
}

这样做有四大好处:

  1. 逻辑复用 :多个组件可共用同一套方法;
  2. 便于测试 :纯函数更容易单元测试;
  3. 提高可读性 :组件不再臃肿;
  4. 支持 tree-shaking :未使用的函数不会被打包进去。

最后整个项目的依赖关系如下:

graph TD
  A[Main App] --> B(TableComponent)
  B --> C{Supports Slot?}
  C -->|Yes| D[Custom Actions]
  C -->|No| E[Default Buttons]
  B --> F[Props: data, columns]
  F --> G[External Service]
  G --> H[tableService.js]
  H --> I[CRUD Logic]
  I --> J[Update State]
  J --> K[Re-render View]

层次分明,职责清晰,这才是现代化前端项目的正确打开方式 ✅!


写在最后:从 CRUD 到架构思维的跃迁

回头看,我们从一个简单的 v-for 开始,经历了数据绑定、用户交互、性能优化、组件封装等多个阶段,最终构建出一个具备生产级别的动态表格系统。

但这不仅仅是一次技术练习,更是一次思维方式的升级:

  • 你学会了 如何思考组件边界 ,而不是一味堆砌代码;
  • 你掌握了 性能优化的权衡艺术 ,知道什么时候该用 computed ,什么时候该上虚拟滚动;
  • 你理解了 响应式背后的机制 ,不再把 Vue 当作“黑盒”工具;
  • 你建立了 工程化意识 ,懂得通过模块拆分提升项目可维护性。

这些能力,远比记住某个 API 更有价值。

未来的路还很长——你可以继续探索:

  • 使用 Vuex/Pinia 管理全局状态;
  • 接入真实 API 实现前后端联调;
  • 添加分页、搜索、排序等高级功能;
  • 用 TypeScript 增强类型安全;
  • 写单元测试保障代码质量。

但无论如何,请记住: 优秀的程序员不是写代码最多的人,而是能让系统持续健康生长的人 🌱。

愿你在 Vue 的世界里,越走越远,越走越稳 ❤️。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Vue.js是构建用户界面的主流JavaScript框架,广泛应用于单页应用开发。本资源“vue.js动态表格增加删除修改代码.zip”提供了一个完整的动态表格实现案例,涵盖数据的添加、删除、编辑等交互功能。通过index.html作为入口页面,结合Vue实例管理表格数据与行为逻辑,利用v-for、v-on等指令实现数据绑定与事件响应。js文件夹包含核心业务逻辑,说明文件则帮助开发者快速理解项目结构与运行方式。该示例深入展示了Vue.js的响应式机制、组件化设计及前端交互处理,适合初学者学习与实际项目参考。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐