案例介绍

本教程基于 Vue 2 框架,指导学习者实现一个功能完整的前端记事本应用——“Todo记事本”。通过本案例,学习者将系统掌握以下核心能力:

  • 使用 v-for 指令完成动态列表渲染,并正确绑定 key 属性
  • 基于唯一标识符(ID)实现数据项的安全删除
  • 运用数组高阶方法 filter() 构建不可变式数据更新逻辑
  • 理解索引(index)与业务 ID 的语义差异及其在 UI 序号展示中的正确使用原则
  • 遵循响应式数据更新规范,确保视图与状态严格同步

功能概览

小黑记事本包含四大核心功能模块:

  1. 任务列表渲染:将任务数据以结构化列表形式动态展示
  2. 任务删除:支持通过交互按钮移除指定任务项
  3. 任务新增:提供表单输入与提交机制,向列表追加新任务
  4. 任务统计与清空:实时显示任务总数,并支持一键清空全部任务

开发环境与初始结构

环境要求

  • Vue 2.x(推荐 2.6.14 或以上稳定版本)
  • 开发工具:VS Code(已配置 Vue 相关插件)
  • 项目结构已初始化,包含基础 HTML 模板与 Vue 实例挂载点

初始模板结构

1
2
3
4
5
6
7
8
<div id="app">
<ul>
<li v-for="(item,index) in list" :key="item.id">
{{ index + 1 }}. {{ item.name }}
<button @click="del(item.id)">×</button>
</li>
</ul>
</div>

注意:上述代码为示意结构,实际开发中需在 Vue 实例中定义数据与方法后方可生效。

功能一:任务列表渲染

数据建模

在 Vue 实例的 data 选项中,定义任务列表 list。遵循数组包裹对象的数据结构规范,每个对象必须包含两个属性:

  • id:数值型唯一标识符,用于数据操作(如删除),不可重复且不可为空
  • name:字符串类型的任务描述内容
1
2
3
4
5
6
7
8
9
data() {
return {
list: [
{ id: 1, name: '跑步1km' },
{ id: 2, name: '跳绳200次' },
{ id: 3, name: '游泳100米' }
]
}
}

关键原则id 必须由开发者显式维护,不得依赖 v-forindex 参数生成或替代index 仅用于渲染序号等非持久化展示场景。

列表渲染指令

使用 v-for 指令遍历 list 数组,语法格式为:

1
v-for="(item, index) in list"
  • item:当前迭代的对象(即单个任务)
  • index:当前项的零基索引(从 0 开始)
  • list:待遍历的数据源(响应式数组)

key 属性的强制规范

v-for 必须配合 :key 绑定唯一、稳定、可预测的值。此处应绑定 item.id

1
<li v-for="(item, index) in list" :key="item.id">

禁止使用 :key="index"。因 index 在数组增删后会发生变化,导致 Vue 的虚拟 DOM Diff 算法失效,引发状态错乱与性能问题。

序号展示的正确实践

UI 中显示的序号(如 “1.”、“2.”)应基于 index 计算,而非 item.id

1
{{ index + 1 }}. {{ item.name }}

原因分析

  • item.id 是业务主键,其值可能不连续(如删除 ID=2 后,剩余 ID 为 1 和 3)
  • 若直接使用 {{ item.id }},UI 将显示 “1.”、“3.”,违反用户对自然序号的预期
  • index 反映当前渲染位置,index + 1 恒为连续正整数,符合展示需求

功能二:任务删除

事件绑定与参数传递

为删除按钮绑定点击事件处理器,并将当前任务的 id 作为参数传入:

1
<span @click="del(item.id)">×</span>

禁止传递 index。因 index 具有时效性,在列表动态变化后不再准确对应原始数据项。

删除方法实现

methods 选项中定义 del 方法,接收 id 参数:

1
2
3
4
5
methods: {
del(id) {
this.list = this.list.filter(item => item.id !== id);
}
}

核心逻辑解析

  • filter() 方法创建一个新数组,不修改原数组,符合 Vue 响应式系统对数组变更检测的要求
  • 箭头函数 item => item.id !== id 定义保留条件:仅当任务 id 不等于待删除 id 时,该任务被保留在新数组中
  • 赋值语句 this.list = ... 将过滤后的新数组重新赋给 this.list,触发 Vue 的响应式更新机制

常见错误警示

错误写法 问题说明
this.list.splice(index, 1) 依赖 index,删除后 index 失效;且 splice 直接修改原数组,虽可触发更新,但语义不清晰、易出错
delete this.list[index] delete 仅删除属性,不改变数组长度,导致空位(undefined),破坏响应式追踪
this.list = this.list.filter(item => item.id === id) 逻辑反向

功能三:任务添加

本节将实现任务列表的新增功能,作为列表管理功能的重要组成部分。该功能需满足以下核心要求:

  • 支持用户在输入框中输入任务名称;
  • 点击“添加任务”按钮后,将新任务插入至任务列表最前端
  • 新增任务须具备唯一标识(id)和名称(name)两个必需属性;
  • 输入为空时禁止提交,并给予明确提示;
  • 提交成功后自动清空输入框。

关键设计原则:Vue 采用数据驱动视图机制。所有 DOM 更新均由数据变更触发,不得直接操作 DOM 元素。新增任务的本质是向响应式数组 list 中插入新对象,视图将自动同步更新。

步骤一:使用 v-model 实现输入框双向绑定

为实时获取并同步用户输入内容,需将输入框与组件数据属性建立双向绑定关系。

  • 在组件 data 选项中声明一个字符串类型的数据属性:

    1
    2
    3
    4
    5
    6
    7
    data() {
    return {
    list: [
    ],
    todoName: '' // 用于绑定输入框的响应式变量
    }
    }
  • 在模板中定位输入框元素(通常位于表单头部),为其添加 v-model 指令:

    1
    <input type="text" placeholder="请输入任务" v-model="todoName">

说明v-model 是 Vue 的语法糖,等价于同时绑定 :value@input 事件。绑定后,todoName 的值将与输入框内容实时、双向同步。修改任一端,另一端立即响应。

步骤二:注册点击事件并实现数组前置插入

新增逻辑的核心是:点击按钮时,将新任务对象插入 list 数组最前端

  • 为“添加任务”按钮绑定 @click 事件处理器:

    1
    <button @click="addTodo">添加任务</button>
  • 在组件 methods 选项中定义 addTodo 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    methods: {
    addTodo() {
    // 1. 输入校验:去除首尾空白后判断是否为空
    if (this.todoName.trim() === '') {
    alert('请输入任务名称');
    return;
    }
    // 2. 构造新任务对象
    const newTodo = {
    id: +new Date(), // 使用时间戳确保 id 唯一性(数值型)
    name: this.todoName
    };
    // 3. 使用 unshift() 方法将新对象插入数组最前端
    this.list.unshift(newTodo);
    // 4. 清空输入框
    this.todoName = '';
    }
    }

重点说明

  • unshift() 是 JavaScript 数组原生方法,用于在数组开头添加一个或多个元素,并返回新长度;
  • +new Date()Date 对象强制转换为毫秒级时间戳数值,可作为临时唯一 id 使用;
  • this.todoName.trim() === '' 用于排除仅含空白字符(空格、制表符、换行符)的无效输入;
  • 清空操作 this.todoName = '' 依赖 v-model 的双向绑定特性,执行后输入框内容自动归零。

常见问题与注意事项

  • id 的生成策略:当前使用时间戳是前端简易方案,适用于演示与原型开发。生产环境必须由后端服务分配全局唯一 ID(如 UUID、数据库自增主键),以避免并发冲突与数据一致性风险。
  • unshift()push() 的选择:本教程采用 unshift() 实现“新增至顶部”,若需“新增至底部”,应替换为 push() 方法。
  • key 属性的必要性v-for 指令中必须为每个列表项指定唯一的 :key(此处为 item.id)。这是 Vue 渲染优化的关键机制,缺失将导致状态错乱与性能下降。
  • 输入校验的严谨性trim() 方法仅去除首尾空白,不处理中间多余空格。如需更严格校验,可结合正则表达式(例如 /^\s*$/)或引入表单验证库。
  • 用户体验增强建议:实际项目中,应使用 Element UIAnt Design Vue 等组件库的 Message 组件替代原生 alert(),以提供非阻塞、样式统一的提示反馈。

功能四:底部功能实现

本节完成小黑记事本应用的底部交互功能,包括任务数量统计与全部清空操作。所有实现均基于 Vue 2 的响应式数据驱动机制,严格遵循“数据变化 → 视图自动更新”的核心原则。

任务数量统计功能

  • 在底部左侧区域动态显示当前待办任务总数;
  • 该数值必须实时反映 list 数组的实际长度;
  • 禁止使用静态数字(如硬编码为 2),须通过数据绑定实现动态计算。

实现原理

  • Vue 2 中,数组实例的 length 属性为响应式属性;
  • list 数组内容发生变化(如 pushsplice 等操作)时,list.length 的值将自动更新;
  • 模板中直接使用插值表达式 {{ list.length }} 即可实现双向绑定。
1
2
3
4
<div class="footer">
<span>合计:{{ list.length }}</span>
<button>清空任务</button>
</div>
验证逻辑
  • 初始状态:list = [{...}, {...}] → 显示 合计:2
  • 新增任务后(如调用 list.unshift({}))→ list.length 自动变为 3 → 视图立即更新为 合计:3
  • 关键结论list.length 是响应式数据源,无需额外监听或计算属性即可直接使用。

全部清空功能

  • 点击“清空任务”按钮后,移除 list 数组中所有元素;
  • 清空操作需触发视图同步更新,确保列表区域完全空白;
  • 清空行为应直接修改原数组引用,而非仅创建新数组。

实现原理

  • Vue 2 的响应式系统依赖于对原始数据对象的直接修改;
  • 使用 this.list = [] 可直接替换原数组引用,触发依赖更新;
  • 禁止使用 this.list.splice(0) 以外的非响应式方式(如 this.list.length = 0 在部分 Vue 2 版本中存在兼容性风险)
  • 推荐统一采用赋值空数组的方式,语义清晰且完全响应式。
1
2
3
4
<div class="footer">
<span>合计:{{ list.length }}</span>
<button @click="clear">清空任务</button>
</div>

方法定义(JavaScript 部分)

1
2
3
4
5
methods: {
clear() {
this.list = [];
}
}

注意事项

  • 不得使用 list = [](缺少 this. 前缀),否则无法触发响应式更新;
  • 不得使用 list.length = 0,该操作在 Vue 2 中不保证触发视图更新
  • 所有对 list 的修改必须通过 this.list 访问,确保被 Vue 的响应式系统追踪。

底部区域条件显示优化

  • list 数组为空(即 list.length === 0)时,整个底部区域(.footer)应隐藏;
  • list 数组非空时,底部区域应正常显示;
  • 该显示/隐藏状态需随 list 长度动态切换,属于典型的条件渲染场景。

实现方案:v-show 指令

  • 选用 v-show 而非 v-if,原因如下:
    • v-show 仅控制 CSS display 属性,切换开销小;
    • 底部结构简单,无复杂初始化逻辑,无需销毁/重建;
    • 更符合频繁切换(增删清空)的性能需求。

模板代码(完整底部结构)

1
2
3
4
<footer class="footer" v-show="list.length > 0">
<span>合计:{{ list.length }}</span>
<button @click="clear">清空任务</button>
</footer>

行为验证

  • 初始 list 为空 → 底部区域不显示;
  • 添加任一任务 → list.length > 0 为真 → 底部显示;
  • 执行 clear()list.length === 0 → 底部立即隐藏;
  • 核心要点v-show 的表达式必须为布尔值,list.length > 0 是最简洁可靠的判断方式。

功能总结与知识点回顾

本案例综合运用 Vue 2 核心指令与响应式机制,完整实现待办清单应用。各模块对应关键技术点如下:

列表渲染

  • 使用 v-for 指令遍历 list 数组;
  • 必须为每个列表项设置唯一 key 属性,推荐使用 item.idindex(仅限无序静态列表);
  • 通过插值表达式 {{ item.text }} 渲染内容;
  • 数据驱动视图:list 变化 → 列表自动重绘。

删除功能

  • 通过 @click 事件绑定删除方法,并传递当前项索引或唯一标识;
  • 在方法体内调用 Array.prototype.filter() 创建新数组;
  • filter() 返回新数组,必须显式赋值给 this.list 才能触发响应式更新
  • 示例:this.list = this.list.filter(item => item.id !== targetId);

添加功能

  • 使用 v-model 实现输入框双向绑定(绑定至 data 中的临时字段,如 inputValue);
  • 在添加方法中调用 Array.prototype.unshift() 将新任务插入数组头部;
  • unshift() 为原地修改方法,可直接触发响应式更新

底部功能

  • 任务统计依赖 list.length 的响应式特性
  • 清空操作通过 this.list = [] 直接重置数组引用
  • 条件显示采用 v-show="list.length > 0" 实现高效切换

开发规范与最佳实践

  • 所有数据操作必须通过 this.xxx 访问,确保被 Vue 实例代理;
  • 避免直接修改数组 length 属性或使用非响应式方法(如 arr[0] = ...);
  • 条件渲染优先级:简单布尔判断 → v-show;复杂结构或高开销组件 → v-if
  • 模板中禁止出现业务逻辑运算,复杂表达式应提取至计算属性(computed);
  • 本案例未使用计算属性,因 list.length 本身已具备响应式能力,无需额外封装。

完整代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记事本 - Vue 2 教程</title>
</head>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<body>
<div id="app">
<input type="text" placeholder="请输入任务" v-model="todoName">
<button @click="addTodo">添加任务</button>
<ul>
<li v-for="(item,index) in list" :key="item.id">
{{ index + 1 }}. {{ item.name }}
<button @click="del(item.id)">×</button>
</li>
</ul>
<footer class="footer" v-show="list.length > 0">
<span>合计:{{ list.length }}</span>
<button @click="clear">清空任务</button>
</footer>
</div>

<script>
new Vue({
el: '#app',
data() {
return {
list: [
{ id: 1, name: '跑步1km' },
{ id: 2, name: '跳绳200次' },
{ id: 3, name: '游泳100米' }
],
todoName: '' // 用于绑定输入框的响应式变量
}
},
methods: {
del(id) {
this.list = this.list.filter(item => item.id !== id);
},
addTodo() {
// 1. 输入校验:去除首尾空白后判断是否为空
if (this.todoName.trim() === '') {
alert('请输入任务名称');
return;
}
// 2. 构造新任务对象
const newTodo = {
id: +new Date(), // 使用时间戳确保 id 唯一性(数值型)
name: this.todoName
};
// 3. 使用 unshift() 方法将新对象插入数组最前端
this.list.unshift(newTodo);
// 4. 清空输入框
this.todoName = '';
},
clear() {
this.list = [];
}
}
});
</script>
</body>
</html>