在前端开发中,Todos 任务清单是一个经久不衰的经典案例。它几乎涵盖了大多数交互场景:列表渲染、条件展示、双向绑定、事件处理、状态统计以及批量操作。通过这个小项目,我们可以清晰地感受到 Vue 的核心思想——数据驱动视图。
传统的前端开发往往围绕“如何操作 DOM”展开:找到元素、修改样式、更新文本……而 Vue 让我们把注意力从“操作页面”转向“管理数据”。一旦数据发生变化,Vue 会自动完成 DOM 的更新。这正是响应式编程的魅力所在。
本文将基于 Vue 3 的 Composition API(组合式 API),完整实现一个功能齐全的 Todos 应用。我们会重点讲解:
- 响应式数据的声明(ref)
- 计算属性(computed)的缓存机制与高级用法(getter/setter)
- 常用指令:v-model、v-for、v-if、:class
- 事件绑定与键盘修饰符
最终代码简洁高效,适合初学者学习,也能给有经验的开发者带来一些思考。
项目完整代码
先直接上完整代码(基于 Vue 3 + Composition API + 单文件组件):
vue
<template>
<div class="todo-app">
<h2>{{ title }}</h2>
<input
type="text"
v-model="newTitle"
@keydown.enter="addTodo"
placeholder="输入任务,按回车添加"
/>
<ul v-if="todos.length">
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" v-model="todo.done">
<span :class="{ done: todo.done }">{{ todo.title }}</span>
</li>
</ul>
<div v-else class="empty">暂无计划</div>
<div class="footer">
<label>
全选
<input type="checkbox" v-model="allDone">
</label>
<div>
{{ active }} / {{ todos.length }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 当前编辑中的标题(用于输入框)
const newTitle = ref('')
// 任务列表
const todos = ref([
{ id: 1, title: '打王者', done: false },
{ id: 2, title: '吃饭', done: false },
{ id: 3, title: '睡觉', done: false },
{ id: 4, title: '学习 Vue3', done: false }
])
// 未完成任务数量(计算属性,带缓存)
const active = computed(() => {
return todos.value.filter(todo => !todo.done).length
})
// 添加任务
const addTodo = () => {
if (!newTitle.value.trim()) return
todos.value.push({
id: Date.now(), // 推荐使用时间戳或自增 ID
title: newTitle.value.trim(),
done: false
})
newTitle.value = ''
}
// 全选 / 全不选(computed 的高级用法:带 getter 和 setter)
const allDone = computed({
get() {
return todos.value.length > 0 && todos.value.every(todo => todo.done)
},
set(val) {
todos.value.forEach(todo => {
todo.done = val
})
}
})
</script>
<style scoped>
.todo-app {
max-width: 600px;
margin: 40px auto;
font-family: system-ui, sans-serif;
}
input[type="text"] {
width: 100%;
padding: 12px;
font-size: 16px;
margin-bottom: 20px;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
li input[type="checkbox"] {
margin-right: 12px;
}
.done {
color: #999;
text-decoration: line-through;
}
.empty {
text-align: center;
color: #999;
padding: 40px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
</style>
从传统思维到响应式思维
在学习 Vue 之前,很多开发者习惯了 jQuery 式的开发方式:
JavaScript
// 传统方式示例
document.querySelector('input').addEventListener('keydown', e => {
if (e.key === 'Enter') {
const value = e.target.value
if (value) {
const li = document.createElement('li')
li.innerHTML = `<input type="checkbox"> <span>${value}</span>`
document.querySelector('ul').appendChild(li)
e.target.value = ''
}
}
})
这种方式的核心是:找到元素 → 操作元素。代码中充斥着查询、创建、插入、删除等 DOM 操作。随着功能增多,代码容易变得混乱,维护成本高。
Vue 的方式完全不同。我们不再关心“页面上哪个元素要改”,而是关心“数据如何变化”。例如:
- 用户输入新任务 → 修改 newTitle 数据
- 按回车 → 向 todos 数组 push 新对象
- Vue 自动检测数据变化,重新渲染列表
这种声明式的编程方式,让代码更聚焦业务逻辑,也更易于测试和维护。
核心指令详解
1. 双向绑定:v-model
HTML
<input type="text" v-model="newTitle" @keydown.enter="addTodo">
v-model 是 Vue 中最常用的指令之一,它在 input 上实现了“值绑定 + input 事件监听”的组合。我们无需手动 addEventListener('input') 来同步值,Vue 帮我们完成了这一切。
2. 列表渲染:v-for
HTML
<li v-for="todo in todos" :key="todo.id">
v-for 基于数组渲染列表。:key 是 Vue 虚拟 DOM diff 的关键,推荐使用唯一且稳定的 ID(如数据库主键或时间戳),避免使用 index(因为插入/删除时 index 会变化,导致不必要的重新渲染)。
3. 条件渲染:v-if / v-else
HTML
<ul v-if="todos.length">...</ul>
<div v-else class="empty">暂无计划</div>
当任务列表为空时显示提示文案。这比 v-show(仅控制 display)更彻底,因为 v-if 会完全销毁/重建元素,适合不经常切换的场景。
4. 动态类绑定::class
HTML
<span :class="{ done: todo.done }">{{ todo.title }}</span>
:class 可以接受对象语法,键是类名,值是布尔值。任务完成时自动添加 done 类,实现删除线和灰色文字效果。
5. 事件监听与修饰符:@keydown.enter
HTML
@keydown.enter="addTodo"
@ 是 v-on: 的缩写。.enter 是 Vue 提供的键盘修饰符,常见修饰符还有 .esc、.tab、.delete、.space 等。使用修饰符可以让事件处理更精确,避免在函数内部写冗长的 if (e.key === 'Enter') 判断。
计算属性(computed)的威力
普通计算属性:统计未完成任务
JavaScript
const active = computed(() => {
return todos.value.filter(todo => !todo.done).length
})
computed 与普通函数的区别在于缓存机制。只有当依赖的响应式数据(这里是 todos)发生变化时,才会重新计算。模板中多次使用 {{ active }} 也不会导致多次 filter 执行,性能更优。
如果用普通方法(methods)实现,每次渲染都会重新计算,即使数据没变。
高级用法:带 setter 的 computed(全选功能)
JavaScript
const allDone = computed({
get() {
return todos.value.length > 0 && todos.value.every(todo => todo.done)
},
set(val) {
todos.value.forEach(todo => { todo.done = val })
}
})
这是 computed 中非常实用的技巧。当我们把 allDone 绑定到复选框时:
- 读取值时执行 get:判断是否全部完成
- 修改值时执行 set:批量设置所有任务的 done 状态
这样就用一行模板代码实现了“全选/全不选”功能,逻辑清晰且响应式。
小贴士:如果列表为空,全选框应为未选中状态,因此 get 中加入了 todos.value.length > 0 判断。
响应式基础:ref 与 reactive
在本例中我们主要使用了 ref:
- ref 适合包装基本类型(如字符串、数字)
- 对于对象/数组,也可以用 ref,访问时需要 .value
- 另一种选择是 reactive,它直接返回响应式对象,无需 .value
两种方式各有优势:
JavaScript
// ref 方式
const todos = ref([])
todos.value.push(...)
// reactive 方式
const state = reactive({ todos: [] })
state.todos.push(...)
在复杂项目中,推荐根据场景选择。Composition API 的灵活性让我们可以把相关逻辑组织在一起,而不是散落在 data、methods、computed 等选项中。
总结
通过这个简单的 Todos 应用,我们可以看到 Vue 3 Composition API 的强大之处:
- 响应式数据驱动一切,开发者只需关注数据变化
- 计算属性提供高效缓存和优雅的双向绑定能力
- 指令系统极大简化了常见交互的实现
- 组合式 API让代码组织更灵活、可复用性更高
从“操作 DOM”到“管理数据”,这种思维转变是学习 Vue(乃至现代前端框架)的关键一步。