玩转Vue3响应式:手把手实现TodoList,掌握核心指令
前言:为什么需要响应式?
在前端开发中,我们经常遇到这样的场景:当数据变化时,需要手动更新DOM。这种手动操作不仅繁琐,还容易出错。Vue的响应式系统正是为了解决这个问题而生------数据变,视图自动变!
今天,我们通过一个TodoList示例,深入浅出地讲解Vue3的核心指令和响应式API。
一、项目概览:一个简洁的TodoList
我们先来看看最终实现的效果:
- 输入任务,回车添加
- 勾选完成任务
- 显示完成进度
- 支持全选/全不选
二、核心指令详解
1. v-model:双向数据绑定
vue
<input type="text" v-model="title" @keydown.enter="addTodo">
什么是双向绑定?
- 单向绑定:数据变化 → 视图更新
- 双向绑定:数据变化 ⇌ 视图更新
v-model的本质:
vue
<!-- v-model 实际上是语法糖 -->
<input
:value="title"
@input="title = $event.target.value"
>
<!-- 等价于 -->
<input v-model="title">
实战技巧:
vue
<!-- 修饰符让开发更高效 -->
<input v-model.trim="title"> <!-- 自动去除首尾空格 -->
<input v-model.number="age"> <!-- 自动转为数字 -->
<input v-model.lazy="content"> <!-- 失焦时更新 -->
2. v-if 和 v-else:条件渲染
vue
<ul v-if="todos.length">
<!-- 有任务时显示列表 -->
</ul>
<div v-else>
没有任务 <!-- 无任务时显示提示 -->
</div>
v-if vs v-show:
v-if:条件为假时,元素从DOM中移除v-show:只是切换display: none- 如何选择? :频繁切换用
v-show,不常变化用v-if
vue
<!-- 多个条件分支 -->
<div v-if="status === 'loading'">加载中...</div>
<div v-else-if="status === 'error'">出错了</div>
<div v-else>加载完成</div>
3. v-for:列表渲染
vue
<li v-for="todo in todos" :key="todo.id">
<!-- 循环渲染 -->
</li>
为什么需要:key? Vue通过key识别节点身份,实现高效的DOM更新。没有key时,Vue会使用"就地复用"策略,可能导致状态错乱。
正确用法:
vue
<!-- 使用唯一标识作为key -->
<li v-for="todo in todos" :key="todo.id">
<!-- 获取索引 -->
<li v-for="(todo, index) in todos" :key="todo.id">
第{{ index + 1 }}项:{{ todo.title }}
</li>
4. v-bind:动态绑定属性
vue
<!-- 完整写法 -->
<span v-bind:class="{'done': todo.done}">{{ todo.title }}</span>
<!-- 简写(常用) -->
<span :class="{'done': todo.done}">{{ todo.title }}</span>
动态Class的多种写法:
vue
<!-- 对象语法 -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- 数组语法 -->
<div :class="[activeClass, errorClass]"></div>
<!-- 结合三目运算符 -->
<div :class="isActive ? 'active' : ''"></div>
动态Style绑定:
vue
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
三、响应式API:ref 和 computed
1. ref:创建响应式数据
javascript
import { ref } from "vue";
const title = ref("") // 字符串
const count = ref(0) // 数字
const todos = ref([]) // 数组
ref的特点:
- 返回一个响应式对象,通过
.value访问实际值 - 在模板中自动解包,无需
.value - 适合基础类型和对象引用
javascript
// 在script中访问
title.value = "新任务" // 需要.value
// 在template中自动解包
<!-- 直接使用,无需.value -->
<input v-model="title">
2. computed:计算属性
计算属性的优势:
- 缓存机制:依赖不变时,直接返回缓存结果
- 响应式:依赖变化时自动重新计算
- 简洁:模板中无需复杂逻辑
javascript
import { computed } from 'vue'
// 基本用法(只读)
const active = computed(() => {
return todos.value.filter(todo => !todo.done).length
})
// 高级用法(getter/setter)
const allDone = computed({
get() {
// 所有todo都完成时返回true
return todos.value.every(todo => todo.done)
},
set(val) {
// 设置所有todo的完成状态
todos.value.forEach(todo => todo.done = val)
}
})
四、事件处理
vue
<!-- 基础事件 -->
<button @click="addTodo">添加</button>
<!-- 键盘事件 -->
<input @keydown.enter="addTodo">
<input @keyup.esc="clearInput">
<!-- 事件修饰符 -->
<form @submit.prevent="onSubmit"> <!-- 阻止默认行为 -->
<div @click.stop="handleClick"> <!-- 阻止事件冒泡 -->
常用修饰符:
.stop- 阻止事件冒泡.prevent- 阻止默认行为.self- 只在元素自身触发.once- 只触发一次.passive- 提升滚动性能
五、完整实现与最佳实践
vue
<script setup>
import { ref, computed } from "vue"
// 1. 定义响应式数据
const title = ref("")
const todos = ref([
{ id: 1, title: "学习Vue3", done: true },
{ id: 2, title: "写项目", done: false }
])
// 2. 定义方法
const addTodo = () => {
if (!title.value.trim()) return
todos.value.push({
id: Date.now(), // 更好的ID生成方式
title: title.value.trim(),
done: false
})
title.value = '' // 清空输入框
}
// 3. 定义计算属性
const activeCount = computed(() =>
todos.value.filter(todo => !todo.done).length
)
const allDone = computed({
get: () => todos.value.every(todo => todo.done),
set: (val) => todos.value.forEach(todo => todo.done = val)
})
</script>
<template>
<div class="todo-container">
<h2>Todo List</h2>
<!-- 输入区域 -->
<div class="input-group">
<input
type="text"
v-model="title"
@keydown.enter="addTodo"
placeholder="输入任务,回车添加"
class="todo-input"
>
</div>
<!-- 任务列表 -->
<div v-if="todos.length" class="todo-list">
<ul>
<li v-for="todo in todos" :key="todo.id" class="todo-item">
<input
type="checkbox"
v-model="todo.done"
class="todo-checkbox"
>
<span
:class="['todo-text', { 'todo-done': todo.done }]"
@dblclick="todo.done = !todo.done"
>
{{ todo.title }}
</span>
</li>
</ul>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
🎉 恭喜!所有任务都完成了!
</div>
<!-- 统计区域 -->
<div class="stats">
<label class="select-all">
<input type="checkbox" v-model="allDone">
全选
</label>
<div class="progress">
{{ activeCount }} / {{ todos.length }}
<span v-if="todos.length">
({{ Math.round((todos.length - activeCount) / todos.length * 100) }}%)
</span>
</div>
</div>
</div>
</template>
<style scoped>
.todo-container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.todo-input {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
}
.todo-list {
margin: 20px 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 12px;
background: white;
border-radius: 8px;
margin-bottom: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.todo-checkbox {
margin-right: 12px;
transform: scale(1.2);
}
.todo-text {
flex: 1;
cursor: pointer;
transition: all 0.3s;
}
.todo-done {
text-decoration: line-through;
color: #888;
opacity: 0.7;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
font-size: 18px;
}
.stats {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20px;
border-top: 1px solid #eee;
}
.select-all {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.progress {
font-weight: bold;
color: #42b883;
}
</style>
请看大屏幕:

六、学习心得与避坑指南
常见问题及解决方案:
-
为什么修改了数据,视图不更新?
- 检查是否使用了
.value(在script中) - 检查数组操作是否使用响应式方法(
push、pop等)
- 检查是否使用了
-
v-for 和 v-if 一起使用?
vue<!-- 错误:v-for优先级高于v-if --> <li v-for="todo in todos" v-if="!todo.done"> <!-- 正确:使用计算属性过滤 --> <li v-for="todo in activeTodos" :key="todo.id"> <!-- 或使用template包裹 --> <template v-for="todo in todos"> <li v-if="!todo.done" :key="todo.id"> </template> -
性能优化建议:
- 大数据列表使用虚拟滚动
- 复杂计算使用computed缓存
- 事件处理使用防抖/节流
总结
Vue3的响应式系统让我们可以更专注于业务逻辑,而不是DOM操作。通过本文的TodoList示例,我们掌握了:
- 数据驱动:用数据描述UI状态
- 指令系统:v-model、v-if、v-for、v-bind的灵活运用
- 响应式API:ref和computed的强大功能
- 最佳实践:写出可维护的Vue代码
记住,Vue的核心思想是"数据驱动视图"。当你理解了数据如何影响视图,就能写出更优雅、更高效的Vue代码。
现在,尝试扩展这个TodoList吧!可以添加:
- 任务分类
- 本地存储
- 拖拽排序
- 过滤功能
祝你编码愉快! 🚀