从“操作 DOM”到“操作数据”:用 Vue 3 实现响应式任务清单
前端开发的本质,不是操控页面元素,而是管理数据状态。
在学习 Vue 之前,我写过很多“传统”的 JavaScript 页面:先用 document.getElementById 找到按钮,再用 addEventListener 绑定点击事件,然后手动创建 <li> 元素、设置文本、追加到 <ul> 中……整个过程像在指挥一群木偶,每一步都要精确控制。
但这种方式繁琐、易错、难以维护——尤其是当页面交互复杂时,DOM 操作代码会迅速膨胀,逻辑和视图高度耦合。
直到我接触了 Vue,才真正体会到:原来前端开发可以如此简洁、高效、声明式。
今天,我就以一个简单的“待办事项(Todo List)”应用为例,对比传统 DOM 编程与 Vue 响应式开发的差异,并深入分析我在 App.vue 中使用的核心特性:v-for、v-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() { /* ... */ }
}
}
- 代码按“选项”组织:
data、methods、computed等各自独立。 - 当组件逻辑复杂时,相关功能(比如“任务管理”)可能分散在多个选项中,难以复用和维护。
组合式 API(Composition API)
// Vue 3 <script setup>
const title = ref('');
const todos = ref([...]);
const active = computed(() => { /* ... */ });
const addTodo = () => { /* ... */ };
- 代码按逻辑关注点组织:所有与“添加任务”相关的变量和函数可以放在一起。
- 利用
ref、computed、watch等函数,逻辑可抽离为自定义 Hook(如useTodos.js) ,极大提升复用性。 <script setup>进一步简化语法,无需return,顶层变量/函数自动暴露给模板。
在我们的
App.vue中,title、todos、active、allDone、addTodo都是直接在<script setup>中声明的响应式变量和函数,模板可以直接使用——这正是组合式 API 的简洁与强大之处。
三、Vue 方式:声明式响应式开发(聚焦数据本身)
现在,看看我在
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 }}但是这样的代码却有很大的缺陷:
- 当页面内的数据发生变化,无论是不是响应式的数据,或者这个数据和你这个表达式毫无关联,页面都会全部重新渲染,导致性能浪费和糟糕的用户体验。
- 而
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,让这种表达变得优雅而高效。