从零实现一个 Vue Todos 任务清单:深入响应式编程与组合式 API

用 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>

这段代码做了什么?

  1. 通过 getElementById 找到 DOM 元素。
  2. 给输入框绑定 change 事件。
  3. 事件回调里手动修改 app 元素的 innerHTML

这是典型的命令式编程:你告诉浏览器"先找元素,再改内容,如果输入为空就忽略"。每一步都要你自己去写。当需求变多(比如添加删除、标记完成、统计未完成数量),命令式代码会迅速膨胀,变成一团难以维护的"意大利面"。

Vue 的做法 截然不同:

你不再思考"如何操作 DOM",而是思考"数据如何变化"。DOM 只是数据状态的映射。

用 Vue 写同样的功能,核心代码只有两行(数据 + 修改数据),DOM 更新完全自动。

二、Vue Todos 是什么?它带来了什么影响?

Vue Todos 是一个基于 Vue.js 框架实现的待办事项管理应用。它通常包含以下功能:

  • 添加新任务
  • 标记任务完成/未完成
  • 显示未完成任务数量
  • 全选/取消全选
  • 任务列表展示

它虽然简单,却是学习 Vue 的经典入门项目,其影响体现在:

  • 教学价值 :覆盖了 Vue 最常用的特性(v-modelv-forv-ifcomputed、事件处理、样式绑定)。
  • 思想传递:让你从 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-ifv-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 类

:classv-bind:class 的缩写。这里传入一个对象,键是类名 done,值是表达式 todo.done。当 todo.done 为真时,<span> 上就会有 done 类,从而应用样式(灰色 + 删除线)。

计算属性展示统计

vue 复制代码
{{ active }} / {{ todos.length }}

active 是一个计算属性,展示未完成的任务数。下面会详细解析。

全选复选框

vue 复制代码
全选<input type="checkbox" v-model="allDone">

allDone 也是一个计算属性,但提供了 getset,实现了"全选/取消全选"的逻辑。

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 = ''

我们只修改了数据(往 todospush 新对象,清空 title)。Vue 自动感知到 todostitle 的变化,并更新 DOM ------ 新任务出现在列表里,输入框变空,未完成计数增加。全程没有一行 DOM 操作代码。

高级计算属性:带 setterallDone

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 触发。
  • 当你点击全选复选框改变其状态时,会触发 setval 是复选框的新值(true/false)。然后 set 中遍历所有任务,批量修改 done 属性。由于 todos 变化,UI 会全部更新。

这种写法非常优雅,你甚至不需要写额外的方法。

3. 样式部分

css 复制代码
.done {
  color: gray;
  text-decoration: line-through;
}

为已完成任务添加灰色和删除线。注意类名 done 与模板中 :class 的对象键一致。

五、Vue 知识扩展与思想升华

1. 响应式原理(简单版)

Vue 3 使用 Proxy 实现响应式。当你 refreactive 一个数据时,Vue 会拦截对该数据的读取和修改操作。读取时收集依赖(比如哪个组件在用这个数据),修改时触发依赖更新。这比 Vue 2 的 Object.defineProperty 更强大,能检测到属性的添加和删除,也能直接代理数组索引和长度变化。

2. Composition API vs Options API

  • Options API (Vue 2 风格):用 datamethodscomputed 等选项组织代码。适合小型组件,但逻辑分散。
  • 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 响应式(声明式)
开发思维 "我要做什么步骤" "数据是什么样,界面就是什么样"
修改界面 手动调用 innerHTMLappendChild 修改数据,Vue 自动更新界面
事件处理 addEventListener,需手动移除 @click 等指令,自动绑定与清理
列表渲染 循环创建元素,维护 DOM 引用 v-for + 唯一 key
状态同步 手动维护数据与 UI 的一致性(易出错) 自动同步,数据是唯一来源
性能优化 需要手动做防抖、节流、减少重绘 虚拟 DOM + diff 算法,批量更新
代码可维护性 随着功能增加,代码变得杂乱 组件化、逻辑集中,易于扩展
学习曲线 低(原生 JS 基础) 中(需要理解响应式、指令等概念)

七、Vue Todos 的使用与效果展示

当你完成上述代码后,你的 Todo 应用具备以下交互:

  1. 添加任务:在输入框打字,按回车,任务添加到列表底部,输入框清空。
  2. 标记完成:点击任务前的复选框,文字立刻变灰并添加删除线;顶部"未完成数量"自动减少。
  3. 全选/全不选:点击"全选"复选框,所有任务同时标记完成/未完成。
  4. 空状态提示:当没有任何任务时,显示"暂无计划"。
  5. 动态统计:实时显示"已完成/总数"。

这一切不需要你写一句 DOM 操作代码。你可以轻松地扩展功能,比如:

  • 添加"删除任务"按钮(只需在 todos 中过滤掉指定 id)。
  • 添加"编辑任务"功能(双击任务文字变成输入框)。
  • 本地持久化(watch 监听 todos 变化并存入 localStorage)。

八、总结与展望

通过这个 Todos 项目,我们不仅学会了 Vue 的核心指令和 API,更重要的是理解了响应式数据驱动这一革命性思想。当你习惯了 Vue 的开发方式,你会发现自己再也回不去"先查 DOM 再改 DOM"的老路------那就像习惯了自动挡汽车后,再去开手摇式拖拉机。

Vue Todos 虽小,却是一面镜子,照出了前端开发从"命令式"到"声明式"、从"手动挡"到"自动挡"的演进历程。如果你能完全吃透这个例子,你已经踏上了 Vue 高手之路。


相关推荐
超绝大帅哥1 小时前
TTFB, FP, FCP, LCP, CLS, INP,TBT, TTI性能指标
前端
用户1733598075371 小时前
纯前端 PDF 处理避坑指南:5 个线上真实问题的解决方案
前端·javascript
Csvn1 小时前
前端项目管理:需求拆解、排期与风险控制
前端
陈_杨1 小时前
鸿蒙APP开发-带你走近分构App的分子数据
前端·javascript
橘子星1 小时前
从零上手!Node.js 快速搭建生成式 AI 后端项目|密钥安全 + 完整可运行代码
前端·后端
陈_杨1 小时前
鸿蒙APP开发-带你开发锻艺册APP的材料清单功能
前端·javascript
xixixin_1 小时前
Promise.all 和 Promise.allSettled 详解
前端·javascript·vue.js
暗冰ཏོ1 小时前
前端数据大屏开发完整指南:Vue3 + ECharts 自适应可视化实战
前端·javascript·echarts·数据大屏·大屏端
陈_杨2 小时前
鸿蒙APP开发-带你了解单块酷APP参数管理的功能
前端·javascript