我删掉了 200 行 DOM 操作代码,只因用了 Vue 3

72 阅读7分钟

从“操作 DOM”到“操作数据”:用 Vue 3 实现响应式任务清单

前端开发的本质,不是操控页面元素,而是管理数据状态。

在学习 Vue 之前,我写过很多“传统”的 JavaScript 页面:先用 document.getElementById 找到按钮,再用 addEventListener 绑定点击事件,然后手动创建 <li> 元素、设置文本、追加到 <ul> 中……整个过程像在指挥一群木偶,每一步都要精确控制。

但这种方式繁琐、易错、难以维护——尤其是当页面交互复杂时,DOM 操作代码会迅速膨胀,逻辑和视图高度耦合。

直到我接触了 Vue,才真正体会到:原来前端开发可以如此简洁、高效、声明式。

今天,我就以一个简单的“待办事项(Todo List)”应用为例,对比传统 DOM 编程与 Vue 响应式开发的差异,并深入分析我在 App.vue 中使用的核心特性:v-forv-model、计算属性(computed)等。


一、传统方式:命令式 DOM 操作(机械而脆弱)

假设我们要实现一个 Todo List,支持添加任务、标记完成、显示统计信息。

用原生 JavaScript,你可能会这样写:

// 1. 获取 DOM 元素
const input = document.getElementById('input');
const list = document.getElementById('list');
const count = document.getElementById('count');

// 2. 维护一个任务数组(但页面不会自动更新!)
let todos = [
  { id: 1, title: '打瓦', done: true },
  { id: 2, title: '吃饭', done: false }
];

// 3. 每次数据变化,都要手动重绘整个列表!
function render() {
  list.innerHTML = ''; // 先清空
  todos.forEach(todo => {
    const li = document.createElement('li');
    li.innerHTML = `
      <input type="checkbox" ${todo.done ? 'checked' : ''}>
      <span style="${todo.done ? 'color:gray; text-decoration:line-through' : ''}">
        ${todo.title}
      </span>
    `;
    list.appendChild(li);
  });
  count.textContent = `未完成:${todos.filter(t => !t.done).length} / 总数:${todos.length}`;
}

// 4. 绑定事件
input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    todos.push({ id: Date.now(), title: input.value, done: false });
    input.value = '';
    render(); // 必须手动调用!
  }
});

render(); // 初始渲染

❌ 问题显而易见:

  • 重复劳动:每次数据变,都要 innerHTML = '' 再重建。
  • 性能差:频繁操作真实 DOM,浏览器要反复重排重绘。
  • 状态分散:数据在 JS 变量里,UI 在 HTML 里,两者靠 render() 脆弱地连接。
  • 难以扩展:加个“全选”功能?又要写一堆 DOM 查询和更新逻辑。

这就是典型的命令式编程:告诉机器“一步一步怎么做”。


二、组合式 API(Composition API) vs 选项式 API(Options API)

在 Vue 3 中,官方推荐使用 <script setup> 语法,它基于 组合式 API(Composition API) 。这与 Vue 2 时代主流的 选项式 API(Options API) 有显著区别。

选项式 API(Options API)

// Vue 2 风格
export default {
  data() {
    return { title: '', todos: [...] };
  },
  computed: {
    active() { /* ... */ }
  },
  methods: {
    addTodo() { /* ... */ }
  }
}
  • 代码按“选项”组织:datamethodscomputed 等各自独立。
  • 当组件逻辑复杂时,相关功能(比如“任务管理”)可能分散在多个选项中,难以复用和维护

组合式 API(Composition API)

// Vue 3 <script setup>
const title = ref('');
const todos = ref([...]);
const active = computed(() => { /* ... */ });
const addTodo = () => { /* ... */ };
  • 代码按逻辑关注点组织:所有与“添加任务”相关的变量和函数可以放在一起。
  • 利用 refcomputedwatch 等函数,逻辑可抽离为自定义 Hook(如 useTodos.js ,极大提升复用性。
  • <script setup> 进一步简化语法,无需 return,顶层变量/函数自动暴露给模板。

在我们的 App.vue 中,titletodosactiveallDoneaddTodo 都是直接在 <script setup> 中声明的响应式变量和函数,模板可以直接使用——这正是组合式 API 的简洁与强大之处。


三、Vue 方式:声明式响应式开发(聚焦数据本身)

image.png 现在,看看我在 App.vue 中用 Vue 3 Composition API 实现的同样功能:

<template>
 <div>
  <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">
    <!-- {{ 数据绑定 表达式结果绑定 }} -->
    <!-- {{ todos.filter(todo=>!todo.done).length }} 
      {{ active }}
      /
    {{ todos.length }}
  </div>
  </div>
</template>

<script setup>
  
import { ref,computed } from 'vue'
// 响应式数据
const title =ref("");
const todos = ref([
 {id:1,title:'打瓦',done:true},
 {id:2,title:'吃饭',done:true},
 {id:3,title:'睡觉',done:true},
 {id:4,title:'学习vue',done:true}
]);

const active = computed(()=>{
  return todos.value.filter(todo=>!todo.done).length;
})
// computed 高级技巧
// get set 属性
const allDone = computed({
  //获取所有todo是否都为done,只有当全部为true时,才会返回true
  get(){
    return todos.value.every(todo=>todo.done);
  },
  //绑定表单,所以参数来自表单,勾选时则为true,设置所有的todo为done
  set(val){
    todos.value.forEach(todo=>todo.done=val);
  }
})
const addTodo = ()=>{
  // focus 数据业务
  if(!title.value) return;
  todos.value.push({
    id:Math.random(),
    title:title.value,
    done:false
  });
  title.value='';
}
</script>

<style scoped>
.done{
  color: gray;
  text-decoration: line-through;
}
</style>

✅ 核心思想转变:

我不再关心“如何更新 DOM”,只关心“数据应该是什么样子”。

Vue 会自动监听 ref 响应式数据的变化,并高效地更新对应的 DOM 片段


四、关键特性解析

1. v-bind 与 @:简洁的属性绑定和事件监听

在 Vue 模板中,我们经常看到这样的写法:

<input v-model="title" @keydown.enter="addTodo">
<span :class="{ 'done': todo.done }">{{ todo.title }}</span>

这里其实用到了两个核心指令:

  • v-bind(缩写为 : :用于动态绑定 HTML 属性或 class/style

    • 例如 :class 就是 v-bind:class 的简写,它把 CSS 类名和 JavaScript 表达式关联起来。
    • 同样,:id="itemId":disabled="isDisabled" 等都属于此类。
    • 它让静态 HTML 变成“数据驱动”的模板。
  • v-on(缩写为 @ :用于监听 DOM 事件

    • @keydown.enter="addTodo" 等价于 v-on:keydown.enter="addTodo"
    • .enter 是 Vue 提供的事件修饰符,表示“只在按下回车键时触发”。
    • 其他常见修饰符如 .stop.prevent.once 等,极大简化了事件处理逻辑。

相比传统方式中手动调用 addEventListener 并判断 event.key === 'Enter',Vue 的 @keydown.enter 更加声明式、简洁且不易出错。


2. v-for:列表渲染的声明式表达

vue
编辑
<li v-for="todo in todos" :key="todo.id">...</li>
  • 作用:基于 todos 数组动态生成列表项。

  • 优势

    • 不用手动 createElement 或拼接字符串。
    • Vue 通过 key(唯一 ID)智能复用和移动 DOM 元素,性能极佳。
    • 数据增删改时,列表自动同步,无需 render()

这就是声明式 UI:你描述“要什么”,而不是“怎么做”。


3. v-model:双向数据绑定的魔法

vue
编辑
<input v-model="title">
<input type="checkbox" v-model="todo.done">
  • 作用:将表单输入与响应式数据自动同步。

  • 原理

    • 输入框值变化 → 自动更新 title
    • title 变化 → 自动更新输入框显示。
  • 效果:省去了 input.value = xxx 和 addEventListener('input', ...) 的样板代码。

数据流变得直观且可预测


4. 计算属性(computed):派生状态的缓存利器

// 计算未完成的任务数量
const active = computed(() => {
  return todos.value.filter(todo => !todo.done).length;
});
  • 为什么不用表达式直接写?
    你可能会很自然地想到这样的代码:

    {{ todos.filter(todo => !todo.done).length }}
    

    但是这样的代码却有很大的缺陷:

    1. 当页面内的数据发生变化,无论是不是响应式的数据,或者这个数据和你这个表达式毫无关联,页面都会全部重新渲染,导致性能浪费和糟糕的用户体验。
    2. 而 computed 只会在依赖的响应式数据发生变化时才重新计算,性能更好。
  • 高级用法:可写的计算属性

    
    const allDone = computed({
      // 获取所有 todo 是否都为 done,只有当全部为 true 时,才会返回 true
      get() {
        return todos.value.every(todo => todo.done);
      },
      // 绑定表单,所以参数来自表单,勾选时则为 true,设置所有的 todo 为 done
      set(val) {
        todos.value.forEach(todo => todo.done = val);
      }
    });
    
    • set() :当我们主动勾选“全选”选项时,由于双向绑定,allDone 自然变为 true,该值作为参数传入 set,从而将所有任务设为已完成。
    • get() :只有当所有任务都完成时才返回 true,此时 allDone 为 true,全选框自动勾选。

    这让“全选”复选框既能反映状态,又能反向控制数据,逻辑集中且健壮。


5. 条件渲染:v-if vs v-show


<ul v-if="todos.length">...</ul>
<div v-else>暂无计划</div>
  • v-if 是“真正的条件渲染”:条件为假时,元素不会存在于 DOM 中
  • 对比 v-show(只是切换 display: none),v-if 更适合运行时条件不太可能改变的场景(如初始为空)。

五、总结:Vue 如何解放开发者?

维度传统 DOM 编程Vue 响应式开发
关注点操作 DOM 元素管理数据状态
代码量多(查询、创建、更新)少(声明模板 + 响应式数据)
性能差(频繁操作真实 DOM)优(虚拟 DOM + 智能 diff)
可维护性低(逻辑分散)高(数据驱动,逻辑集中)
心智负担高(需考虑 DOM 结构)低(只需思考数据流)

“Vue 做法不再需要思考页面的元素怎么操作,而是要思考数据是怎么变化的。”

这正是现代前端框架的核心价值:将开发者从 DOM 操作的泥潭中解放出来,专注于业务逻辑和用户体验。


如果你也厌倦了“找元素 → 改样式 → 绑事件”的老套路,不妨试试 Vue —— 它会让你重新爱上前端开发。

代码不是为了操控机器,而是为了表达意图。
—— 而 Vue,让这种表达变得优雅而高效。