用 Vue 3 重写经典的 TodoMVC,你会发现:原来前端开发可以如此简单、优雅。
引言:为什么是 Todos?
几乎每一个前端开发者入门框架时,都会写一个 Todo 应用。它简单,却涵盖了最核心的增删改查、状态管理、用户交互。而用 Vue 来实现 Todo,更能让你体会到 响应式数据驱动 的真正魅力------你不再需要操心 DOM 操作,只需要关心数据怎么变,剩下的交给 Vue。
本文我将基于 Vue 3 的 Composition API,一步步构建一个功能完整的任务清单应用。我会逐行讲解代码,并扩展讲解背后的 Vue 核心思想。无论你是 Vue 新手还是想深入理解响应式原理,这篇文章都会给你带来收获。
一、传统做法 vs Vue 做法:一场思想革命
先来看一个最原始的 Todo 添加功能,用原生 JavaScript 实现:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>传统做法</title>
</head>
<body>
<h2 id="app"></h2>
<input type="text" id="todo-input">
<script>
const app = document.getElementById('app');
const todoInput = document.getElementById('todo-input');
todoInput.addEventListener('change', function(event) {
const todo = event.target.value.trim();
if (!todo) return;
app.innerHTML = todo;
})
</script>
</body>
</html>
这段代码做了什么?
- 通过
getElementById找到 DOM 元素。 - 给输入框绑定
change事件。 - 事件回调里手动修改
app元素的innerHTML。
这是典型的命令式编程:你告诉浏览器"先找元素,再改内容,如果输入为空就忽略"。每一步都要你自己去写。当需求变多(比如添加删除、标记完成、统计未完成数量),命令式代码会迅速膨胀,变成一团难以维护的"意大利面"。
而 Vue 的做法 截然不同:
你不再思考"如何操作 DOM",而是思考"数据如何变化"。DOM 只是数据状态的映射。
用 Vue 写同样的功能,核心代码只有两行(数据 + 修改数据),DOM 更新完全自动。
二、Vue Todos 是什么?它带来了什么影响?
Vue Todos 是一个基于 Vue.js 框架实现的待办事项管理应用。它通常包含以下功能:
- 添加新任务
- 标记任务完成/未完成
- 显示未完成任务数量
- 全选/取消全选
- 任务列表展示
它虽然简单,却是学习 Vue 的经典入门项目,其影响体现在:
- 教学价值 :覆盖了 Vue 最常用的特性(
v-model、v-for、v-if、computed、事件处理、样式绑定)。 - 思想传递:让你从 jQuery 式的 DOM 操作思维,平滑过渡到数据驱动思维。
- 工程基础:几乎所有中大型 Vue 项目的架构(组件化、状态管理)都可以从 Todo 中看到影子。
实际使用中,Vue Todos 可以作为一个笔记插件、任务管理工具的基础模板。你只需要扩展后端 API,就能变成一个完整的协作工具。
三、项目搭建与文件结构
我们使用 Vue 3 + Vite 创建项目。入口文件 main.js 非常简洁:
javascript
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
createApp(App)创建一个 Vue 应用实例,传入根组件App。.mount('#app')将应用挂载到index.html中 id 为app的元素上。
一切从 App.vue 开始。它是一个单文件组件 (SFC),包含三部分:<template>、<script setup>、<style>。下面我们逐行深入剖析。
四、App.vue 逐行讲解
1. 模板部分 (Template)
vue
<template>
<div>
<h1></h1>
<!-- 数据绑定 -->
<h2>{{ title }}</h2>
<!-- 双向数据绑定 + 键盘事件 -->
<input type="text" v-model="title" @keydown.enter="addTodo">
<!-- 条件渲染指令 -->
<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>
暂无计划
</div>
<div>
全选<input type="checkbox" v-model="allDone">
{{ active }} / {{ todos.length }}
</div>
</div>
</template>
{{ title }} ------ 插值表达式
{{ }} 是 Vue 的文本插值语法,它会自动将 title 变量的值渲染到该位置,并且当 title 变化时,这里的内容会自动更新。这背后依赖 Vue 的响应式系统。
v-model="title" ------ 双向绑定
v-model 是一个语法糖,本质上是 :value="title" 和 @input="title = $event.target.value" 的合并。用在输入框上,它能实现:
- 数据变化 → 输入框的值变化
- 用户输入 → 数据同步变化
所以你在输入框里打字时,上面的 <h2>{{ title }}</h2> 会实时显示同样的文字。
@keydown.enter="addTodo" ------ 事件监听
@ 是 v-on 的缩写。.enter 是修饰符,表示仅在按下回车键时触发。addTodo 是我们在 <script> 中定义的方法。相比原生 addEventListener,Vue 的事件绑定更语义化,自动处理移除事件等细节。
v-if 与 v-else ------ 条件渲染
vue
<ul v-if="todos.length">
<!-- 列表 -->
</ul>
<div v-else>暂无计划</div>
- 当
todos数组有长度时,渲染<ul>。 - 否则渲染
<div>提示"暂无计划"。
v-if 是真正的条件渲染 ,它会根据条件销毁或重建元素。如果只是需要隐藏/显示,可以用 v-show(保留 DOM 但切换 CSS display)。
v-for ------ 列表渲染
vue
<li v-for="todo in todos" :key="todo.id">
- 遍历
todos数组,每个元素别名todo。 :key是必须的,它给每个列表项一个唯一标识,帮助 Vue 高效地复用和重排 DOM 元素。通常使用数据中的id,避免用索引。
v-model 绑定复选框
vue
<input type="checkbox" v-model="todo.done">
复选框的 v-model 会绑定到一个布尔值。当勾选/取消时,todo.done 自动变为 true/false。这完美体现了数据驱动:UI 改变数据,数据再驱动 UI 的其他部分(比如下面的 :class)。
:class="{done: todo.done}" ------ 动态绑定 CSS 类
:class 是 v-bind:class 的缩写。这里传入一个对象,键是类名 done,值是表达式 todo.done。当 todo.done 为真时,<span> 上就会有 done 类,从而应用样式(灰色 + 删除线)。
计算属性展示统计
vue
{{ active }} / {{ todos.length }}
active 是一个计算属性,展示未完成的任务数。下面会详细解析。
全选复选框
vue
全选<input type="checkbox" v-model="allDone">
allDone 也是一个计算属性,但提供了 get 和 set,实现了"全选/取消全选"的逻辑。
2. 脚本部分 (Script)
vue
<script setup>
import { ref, computed } from 'vue'
const title = ref("")
const todos = ref([
{ id: 1, title: '打王者', done: true },
{ id: 1, title: '吃饭', done: true } // 注意 id 重复了,后面会修正
])
const active = computed(() => {
return todos.value.filter(todo => !todo.done).length
})
const addTodo = () => {
if (!title.value) return
todos.value.push({
id: Math.random(),
title: title.value,
done: false
})
title.value = ''
}
const allDone = computed({
get() {
return todos.value.every(todo => todo.done)
},
set(val) {
todos.value.forEach(todo => todo.done = val)
}
})
</script>
ref ------ 响应式数据的基石
ref 接收一个内部值,返回一个响应式的可变更对象。在模板中直接使用 title,但在 <script> 里需要通过 .value 访问/修改。
为什么需要 .value?因为 JavaScript 基础类型(如字符串、数字)无法被代理,所以 Vue 用一个对象包裹它,通过 value 属性实现响应式。对于对象/数组,ref 内部会调用 reactive 进行深层代理。
computed ------ 计算属性
active 是一个只读的计算属性,它依赖 todos。当 todos 变化时,active 会自动重新计算。更重要的是,计算属性有缓存:只有依赖变化时才重新求值,否则直接返回缓存结果。这比在模板中写方法调用性能更好。
addTodo ------ 添加任务
重点看这两行:
javascript
todos.value.push({
id: Math.random(),
title: title.value,
done: false
})
title.value = ''
我们只修改了数据(往 todos 里 push 新对象,清空 title)。Vue 自动感知到 todos 和 title 的变化,并更新 DOM ------ 新任务出现在列表里,输入框变空,未完成计数增加。全程没有一行 DOM 操作代码。
高级计算属性:带 setter 的 allDone
javascript
const allDone = computed({
get() {
return todos.value.every(todo => todo.done) // 所有任务完成时返回 true
},
set(val) {
todos.value.forEach(todo => todo.done = val) // 设置所有任务的 done 为 val
}
})
- 当你在模板中读取
allDone(比如v-model读取值)时,get触发。 - 当你点击全选复选框改变其状态时,会触发
set,val是复选框的新值(true/false)。然后set中遍历所有任务,批量修改done属性。由于todos变化,UI 会全部更新。
这种写法非常优雅,你甚至不需要写额外的方法。
3. 样式部分
css
.done {
color: gray;
text-decoration: line-through;
}
为已完成任务添加灰色和删除线。注意类名 done 与模板中 :class 的对象键一致。
五、Vue 知识扩展与思想升华
1. 响应式原理(简单版)
Vue 3 使用 Proxy 实现响应式。当你 ref 或 reactive 一个数据时,Vue 会拦截对该数据的读取和修改操作。读取时收集依赖(比如哪个组件在用这个数据),修改时触发依赖更新。这比 Vue 2 的 Object.defineProperty 更强大,能检测到属性的添加和删除,也能直接代理数组索引和长度变化。
2. Composition API vs Options API
- Options API (Vue 2 风格):用
data、methods、computed等选项组织代码。适合小型组件,但逻辑分散。 - Composition API (Vue 3 风格):用
setup函数或<script setup>,将相关逻辑聚合在一起(比如把所有 todo 相关逻辑放在一块)。更适合大型组件和逻辑复用。
本文用的 <script setup> 是 Composition API 的语法糖,代码更简洁。
3. 为什么 v-for 必须搭配 :key?
key 帮助 Vue 识别每个节点的身份。没有 key,Vue 会采用"就地更新"策略,可能导致渲染错误(比如列表顺序变化时)。使用唯一的 key(通常是 id),Vue 可以精确复用和重排元素,提高性能。
4. 计算属性的缓存 vs 方法调用
模板中你也可以写:{{ todos.filter(todo => !todo.done).length }}。但这样每次重新渲染都会执行一次 filter(即使 todos 没变)。而计算属性只在依赖变化时重新计算,然后缓存结果。在复杂逻辑或大型列表时,计算属性性能优势明显。
六、传统 DOM 操作与 Vue 响应式对比表
| 维度 | 传统 DOM 操作(命令式) | Vue 响应式(声明式) |
|---|---|---|
| 开发思维 | "我要做什么步骤" | "数据是什么样,界面就是什么样" |
| 修改界面 | 手动调用 innerHTML、appendChild 等 |
修改数据,Vue 自动更新界面 |
| 事件处理 | addEventListener,需手动移除 |
@click 等指令,自动绑定与清理 |
| 列表渲染 | 循环创建元素,维护 DOM 引用 | v-for + 唯一 key |
| 状态同步 | 手动维护数据与 UI 的一致性(易出错) | 自动同步,数据是唯一来源 |
| 性能优化 | 需要手动做防抖、节流、减少重绘 | 虚拟 DOM + diff 算法,批量更新 |
| 代码可维护性 | 随着功能增加,代码变得杂乱 | 组件化、逻辑集中,易于扩展 |
| 学习曲线 | 低(原生 JS 基础) | 中(需要理解响应式、指令等概念) |
七、Vue Todos 的使用与效果展示
当你完成上述代码后,你的 Todo 应用具备以下交互:
- 添加任务:在输入框打字,按回车,任务添加到列表底部,输入框清空。
- 标记完成:点击任务前的复选框,文字立刻变灰并添加删除线;顶部"未完成数量"自动减少。
- 全选/全不选:点击"全选"复选框,所有任务同时标记完成/未完成。
- 空状态提示:当没有任何任务时,显示"暂无计划"。
- 动态统计:实时显示"已完成/总数"。
这一切不需要你写一句 DOM 操作代码。你可以轻松地扩展功能,比如:
- 添加"删除任务"按钮(只需在
todos中过滤掉指定id)。 - 添加"编辑任务"功能(双击任务文字变成输入框)。
- 本地持久化(
watch监听todos变化并存入localStorage)。
八、总结与展望
通过这个 Todos 项目,我们不仅学会了 Vue 的核心指令和 API,更重要的是理解了响应式数据驱动这一革命性思想。当你习惯了 Vue 的开发方式,你会发现自己再也回不去"先查 DOM 再改 DOM"的老路------那就像习惯了自动挡汽车后,再去开手摇式拖拉机。
Vue Todos 虽小,却是一面镜子,照出了前端开发从"命令式"到"声明式"、从"手动挡"到"自动挡"的演进历程。如果你能完全吃透这个例子,你已经踏上了 Vue 高手之路。