《玩转Vue3响应式:手把手实现TodoList,掌握核心指令》

玩转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>

请看大屏幕:

六、学习心得与避坑指南

常见问题及解决方案:

  1. 为什么修改了数据,视图不更新?

    • 检查是否使用了.value(在script中)
    • 检查数组操作是否使用响应式方法(pushpop等)
  2. 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>
  3. 性能优化建议:

    • 大数据列表使用虚拟滚动
    • 复杂计算使用computed缓存
    • 事件处理使用防抖/节流

总结

Vue3的响应式系统让我们可以更专注于业务逻辑,而不是DOM操作。通过本文的TodoList示例,我们掌握了:

  1. 数据驱动:用数据描述UI状态
  2. 指令系统:v-model、v-if、v-for、v-bind的灵活运用
  3. 响应式API:ref和computed的强大功能
  4. 最佳实践:写出可维护的Vue代码

记住,Vue的核心思想是"数据驱动视图"。当你理解了数据如何影响视图,就能写出更优雅、更高效的Vue代码。

现在,尝试扩展这个TodoList吧!可以添加:

  • 任务分类
  • 本地存储
  • 拖拽排序
  • 过滤功能

祝你编码愉快! 🚀

相关推荐
哆啦A梦15882 小时前
商城后台管理系统 07 商品列表-分页实现
前端·javascript·vue.js
爱因斯坦乐2 小时前
【若依】前后端分离添加导入
java·前端·javascript
Cache技术分享2 小时前
267. Java 集合 - Java 开发必看:ArrayList 与 LinkedList 的全方位对比及选择建议
前端·后端
答案answer2 小时前
Vue3项目集成monaco-editor实现浏览器IDE代码编辑功能
前端·vue.js
爱上妖精的尾巴3 小时前
6-1WPS JS宏 new Set集合的创建
前端·后端·restful·wps·js宏·jsa
绝世唐门三哥3 小时前
Vue 自定义指令完全指南(含 Vue2/Vue3 对比 + 完整 Demo)
前端·javascript·vue.js
uhakadotcom3 小时前
Tomli 全面教程:常用 API 串联与实战指南
前端·面试·github
Asurplus3 小时前
【VUE】15、安装包管理工具yarn
前端·vue.js·npm·node.js·yarn
liangshanbo12153 小时前
Mac M3 安装 Antigravity Agent “已损坏“ 问题解决方案
前端·macos·antigravity