Vue3 Composition API 实战:用响应式思维构建 Todos 任务清单

0 阅读5分钟

在前端开发中,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(乃至现代前端框架)的关键一步。