以前我手动砌 DOM,现在 Vue 给我盖别墅 🏠

81 阅读9分钟

📝 用 Vue 开发 Todos 任务清单

你有没有过这样的经历?想做个简单的任务清单,结果写着写着就陷入了 "找 DOM、改 DOM、DOM 又乱了" 的死循环🤯?今天咱们就来聊聊,从传统 JS 的 "手动搬砖" 到 Vue 的 "数据驱动躺平",开发一个 Todos 任务清单到底能有多爽!

🔙 一、传统做法:被 DOM 支配的恐惧

先回忆一下,没学 Vue 的时候咱们是怎么写任务清单的?就像demo.html里展示的那样,全程被 DOM 牵着鼻子走:

html

<!-- 传统JS操作DOM示例 -->
<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) {
      console.log('请输入任务');
      return;
    }
    app.innerHTML = todo;
  });
</script>

这段代码翻译成人话就是:先费劲吧啦找到<h2>和输入框(getElementById),然后监听输入框的变化(addEventListener),拿到输入内容后,再手动把内容塞给<h2>innerHTML)。这还只是改个标题,要是想加个列表、弄个复选框、统计完成数量... 光是想想那些appendChildremoveChild就头大!

传统 DOM 编程就像用手一块块砌砖:每加一块砖(改一个元素)都得亲自搬、亲自放,稍有不慎就砌歪了(DOM 结构乱了)。效率低不说,还特别容易出错 —— 这大概就是前端开发者早期的 "工伤" 吧😂。

✨ 二、响应式数据驱动:让数据自己 "动" 起来

Vue 的出现,直接把我们从 "搬砖工地" 拉到了 "全自动生产线"!它的核心思想就是响应式数据驱动界面—— 简单说就是:你只管管好数据,界面怎么变,Vue 替你操心。

1、什么是响应式数据驱动?

就像你在手机上改了通讯录名字,通话记录里的名字会自动更新一样:数据变了,依赖这个数据的界面元素会 "聪明地" 自动更新,完全不用你手动操作 DOM。

2、为啥非要用它?

  • 省脑子:不用记哪个 DOM 元素对应哪个数据,不用写一堆xxx.innerHTML
  • 性能好:Vue 会智能追踪数据变化,只更新需要改的地方(比手动操作 DOM 高效多了)
  • 好维护:数据逻辑和界面展示分开,代码像整理好的衣柜,而不是堆成山的衣服

3、Vue 怎么实现响应式数据驱动?

Vue 不会自动监控所有数据,需要你用专属 API 把普通数据 “包装” 成响应式数据,常用的就是 ref 和 reactive

(1) ref:给基本类型数据(字符串 / 数字)装监控

适合单个值(比如标题、数量),底层是 “把基本类型包成对象,再用 Proxy 监控”:

javascript

import { ref } from 'vue';
// 普通字符串 → 响应式数据(带.value的包装对象)
const title = ref("Todos任务清单"); 

// 修改时必须用.value(因为是包装对象)
title.value = '我的今日计划'; 
// 修改后,所有依赖title的界面元素(h2、输入框)自动更新
(2) reactive:给对象 / 数组装监控

适合复杂数据(比如任务列表、用户信息),直接返回 Proxy 代理对象,不用.value

javascript

import { reactive } from 'vue';
// 普通数组 → 响应式数组
const todos = reactive([{ id:1, title:'打王者', done:false }]);

// 直接修改,不用.value
todos.push({ id:2, title:'学Vue' });

💡 小技巧:Todos 案例里用ref包裹数组也能行(const todos = ref([])),本质是 ref 自动把数组转成 reactive 的代理对象,只是需要.value访问。

🚀 三、Todos 任务清单项目:Vue 实战秀

说了这么多,咱们来看看用 Vue 做的 Todos 任务清单长啥样 —— 这可是个 "麻雀虽小,五脏俱全" 的经典案例!

1、项目效果亮个相

  • 顶部有个大标题,还能通过输入框改(双向绑定的魔力✨)
  • 输入框敲回车,就能新增任务(@keydown.enter 立功了)
  • 任务列表里的复选框一点,任务就会变灰加删除线(:class 动态样式)
  • 任务为空时显示 "暂无计划",有任务时才显示列表(v-if/v-else 切换)
  • 底部有全选按钮,还能显示 "已完成数 / 总任务数"(computed 计算属性的功劳)

QQ20251212-11460.gif

2、代码速览

vue

<template>
  <div>
    <h2>
      <!-- 数据绑定 -->
      {{ title }}
    </h2>
    <!-- 双向数据绑定 -->
    <!-- @ v-bind: 的缩写,不用addEventListener -->
    <!-- @event-name.enter 监听键盘输入,当按下回车的时候 -->
    <input type="text" v-model="title" @keydown.enter="addTodo">
    <!-- 条件渲染指令 -->
    <ul v-if="todos.length">
      <!-- key 唯一属性 -->
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.done">
        <!-- // : v-bind:缩写 js表达式 
          vue 有一定的学习 api 对用户非常友好,好上手 
        -->
        <span :class="{done:todo.done}">{{ todo.title }}</span>
      </li>
    </ul>
    <div v-else>
      暂无计划
    </div>
    <div>
      全选<input type="checkbox" v-model="allDone">
      <!-- {{ 数据绑定 表达式结果绑定 }} -->
      <!-- {{ todos.filter(todo => todo.done).length }} -->
      {{ active }}
      /
      {{ todos.length }}
    </div>
  </div>
</template>

<script setup>
  // 业务是界面上要动态展示标题,且编辑标题
  // vue focus 标题数据业务,修改数据,余下的dom更新vue 替我们做了
  // setup vue3的composition 组合式 API
  // vue2 options API
  import {ref,computed} from 'vue'
  // 响应式数据
  const title = ref("Todos任务清单");
  const todos = ref([
    {
      id:1,
      title:'打王者',
      done:false
    },
    {
      id:1,
      title:'吃饭',
      done:true
    }
    
    // '睡觉',
    // '学习Vue'
  ])

  // 依赖于todos 响应式数据的 计算属性
  // 形式上是函数(计算过程),结果(计算属性)返回
  // 也是响应式的 依赖todos
  // computed 缓存性能优化 只有todos 变化时才会重新计算
  const active = computed(() => {
    return todos.value.filter(todo => todo.done).length;
  })
  const addTodo = ()=>{
    // focus 业务
    if(!title.value) {
      return;
    }
    todos.value.push({
       id:Math.random(),
      title: title.value,
      done:false});
    title.value ='';
  }

  // computed 高级技巧
  // get set 属性概念
  const allDone = computed({
    get() {
      return todos.value.every(todo => todo.done);
    },
    set(val) {
      todos.value.forEach(todo => todo.done = val);
    }
  })
</script>

<style>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  body {
    background-color: #ffc0cb;
    min-height: 100vh;
    padding: 20px;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  }

  #app {
    background-color: #ffc0cb;
    min-height: 100vh;
  }

  #app > div {
    display: flex !important;
    flex-direction: column !important;
    align-items: center;
    gap: 20px;
    max-width: 600px;
    margin: 40px auto;
    padding: 30px;
    background-color: #fff;
    border-radius: 20px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  }

  h2 {
    font-size: 28px;
    color: #d63384;
    margin-bottom: 10px;
    text-align: center;
    font-weight: 600;
  }

  input[type="text"] {
    width: 100%;
    padding: 12px 16px;
    font-size: 16px;
    border: 2px solid #ffc0cb;
    border-radius: 10px;
    outline: none;
    transition: all 0.3s ease;
  }

  input[type="text"]:focus {
    border-color: #d63384;
    box-shadow: 0 0 0 3px rgba(214, 51, 132, 0.1);
  }

  ul {
    width: 100%;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }

  li {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px;
    background-color: #fff5f7;
    border-radius: 10px;
    transition: all 0.3s ease;
  }

  li:hover {
    background-color: #ffeef2;
    transform: translateX(5px);
  }

  input[type="checkbox"] {
    width: 20px;
    height: 20px;
    cursor: pointer;
    accent-color: #d63384;
  }

  span {
    flex: 1;
    font-size: 16px;
    color: #333;
    transition: all 0.3s ease;
  }

  .done {
    color: #999;
    text-decoration: line-through;
    opacity: 0.6;
  }

  div[v-else] {
    text-align: center;
    color: #999;
    font-size: 16px;
    padding: 20px;
  }

  div:last-child {
    width: 100%;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding-top: 20px;
    border-top: 2px solid #ffeef2;
    font-size: 16px;
    color: #666;
  }

  div:last-child input[type="checkbox"] {
    margin-right: 5px;
  }
</style>

🔍 四、代码详解:Vue 黑科技大揭秘

接下来咱们拆拆看,这个项目里藏了哪些 Vue 的 "黑科技",让代码这么简洁又强大!

核心指令:Vue 的 "快捷键"

1. 双向数据绑定 v-model —— 数据和界面 "手拉手"

html

<input type="text" v-model="title">

v-model就像个 "传话筒":输入框里的内容变了,title数据会自动跟着变;反过来,title数据改了,输入框显示的内容也会立刻更新。比如你在输入框里敲 "今日计划",上面的<h2>{{ title }}</h2>会同步显示,完全不用写额外代码 —— 这就是双向绑定的快乐!

复选框也能用:

html

<input type="checkbox" v-model="todo.done">

勾选复选框,todo.done会变成truetodo.done设为true,复选框会自动勾选,简直天作之合~

2. 事件监听 v-on(缩写@)—— 给元素装 "感应器"

html

<input type="text" @keydown.enter="addTodo">

@v-on:的缩写,相当于给元素装了个 "感应器"。这里的@keydown.enter表示:" 当用户在输入框按下回车键时,执行addTodo函数 "。

对比传统 JS 的addEventListener('keydown', ...),Vue 的写法直接把事件和处理函数绑在一起,可读性拉满!而且还能加修饰符(比如.enter只监听回车键),不用自己写if(event.key === 'Enter'),细节控狂喜🥳!

3. 条件渲染 v-ifv-else —— 界面的 "开关"

html

<ul v-if="todos.length">...</ul>
<div v-else>暂无计划</div>

这对组合就像个智能开关:当todos数组有内容(todos.length > 0),就显示<ul>列表;否则显示 "暂无计划"。完全不用手动display: none或者删除 DOM,Vue 会根据数据自动切换,省心到想鼓掌!

4. 列表渲染 v-for —— 批量生产元素的 "复印机"

html

<li v-for="todo in todos" :key="todo.id">...</li>

v-for就像个复印机,能根据数组todos里的每个元素,复制出一个<li>。比如todos有 3 个任务,就会生成 3 个<li>,完全不用手动写循环拼字符串~

这里的:key是个小细节(:keyv-bind:key的缩写),它给每个元素一个唯一标识,让 Vue 能准确识别哪个元素变了,更新时更高效 —— 就像给每个学生发个学号,点名时不会认错人😉。

核心 API:Vue 的 "发动机"

1. 响应式 API ref —— 数据的 "超能力"

javascript

const title = ref("Todos任务清单");
const todos = ref([{ id:1, title:'打王者', done:false }]);

ref能让普通数据拥有 "响应式超能力"。注意哦,用ref创建的基本类型数据(比如字符串、数字),修改时要加.value(比如title.value = '新标题');数组或对象虽然也需要.value,但修改内部元素(比如 todos.value.push(...))时,Vue 能自动感知到。

有了ref,我们再也不用关心 "数据变了怎么更界面",专心处理数据逻辑就行 —— 这就是 Vue 的 "数据驱动" 精髓!

2. 计算属性 computed —— 会 "自动算账" 的工具人

javascript

// 统计已完成的任务数
const active = computed(() => {
  return todos.value.filter(todo => todo.done).length;
});

// 全选功能
const allDone = computed({
  get() { return todos.value.every(todo => todo.done); },
  set(val) { todos.value.forEach(todo => todo.done = val); }
});

computed就像个 "自动计算器":它依赖的数据(比如todos)变了,它会自动重新计算结果,而且会缓存计算结果(不是每次用到都算),性能超好!

  • active用来统计已完成的任务数,依赖todos,所以todos里的任务状态变了,active会自动更新
  • allDone更厉害,是个 "读写两用" 的计算属性:get判断是否全选,set在全选框变化时,批量更新所有任务的状态 —— 几行代码就实现了全选功能,简直不要太高效!

🎯 五、总结:Vue 让开发变成 "享受"

回顾一下,从传统 DOM 编程的 "手动搬砖",到 Vue 的 "数据驱动躺平",Todos 任务清单的开发难度直接降了好几个 level:

  • 不用再写一堆getElementByIdaddEventListener,专注数据逻辑就行
  • 响应式数据让界面自动更新,省了 N 行 DOM 操作代码
  • 指令(v-model、v-for 等)和 API(ref、computed)就像现成的 "零件",拼一拼就出功能

其实 Vue 的魅力远不止这些,它就像个贴心的助手,把复杂的 DOM 操作藏在底层,让我们能以更直观、更高效的方式写代码。下次再做项目,试试用 Vue 的思路想想 —— 也许你会发现,前端开发原来可以这么快乐😊!

最后送大家一句话:别和 DOM 死磕,让数据替你说话,Vue 会帮你搞定剩下的一切~ 下次见!👋