别再手动操作 DOM 了!Vue3 CompositionAPI 写 TodoList,面试官看了直呼内行
从“改元素”到“改数据”,前端思维的范式革命
你有没有经历过这样的场景?
“我要加一个待办事项,先
document.querySelector('#list')找到容器,再createElement('li')创建节点,然后设置文本、绑定事件、插入 DOM……”
这种“命令式”编程方式,在 jQuery 时代如鱼得水。但随着应用复杂度飙升,代码逐渐变成“意大利面条”——状态散落各处,逻辑难以追踪,测试寸步难行。
而 Vue 的出现,带来了一场响应式驱动的思维革命:
“你只管关心数据怎么变,DOM 怎么更新?交给 Vue 就行。”
今天,我们就以一个看似简单的 TodoList 为例,深入剖析 Vue3 Composition API 背后的响应式原理、性能优化策略与工程化思维,并关联大厂高频面试题,带你从“会写”进阶到“懂为什么这么写”。
一、核心代码速览:一个“麻雀虽小,五脏俱全”的 TodoList
<template>
<div>
<h2>{{ title }}</h2>
<input 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" />
{{ active }} / {{ todos.length }}
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const title = ref('')
const todos = ref([
{ id: 1, title: '打王者', done: false },
{ id: 2, title: '吃饭', done: true }
])
const active = computed(() => todos.value.filter(t => !t.done).length)
const addTodo = () => {
if (!title.value) return
todos.value.push({
id: Math.random(),
title: title.value,
done: false
})
title.value = ''
}
const allDone = computed({
get() {
return todos.value.every(t => t.done)
},
set(val) {
todos.value.forEach(t => t.done = val)
}
})
</script>
<style>
.done { color: gray; text-decoration: line-through; }
</style>
这段代码,看似平平无奇,实则暗藏玄机。接下来,我们逐层拆解。
二、Composition API:为什么是 ref 而不是 reactive?
问题引入:
在 setup() 中,我们用 ref('') 声明 title,用 ref([...]) 声明 todos。为什么不直接用 reactive 包裹整个状态对象?
深度解析:
-
refvsreactive:ref适用于基础类型(string/number/boolean)或需要频繁替换引用的场景(如数组整体替换)。reactive适用于对象结构稳定的复杂状态。
-
模板自动解包(Unwrapping) : 在
<template>中,{{ title }}实际访问的是title.value,这是 Vue 的语法糖。但若将todos改为reactive({ list: [...] }),则需写v-for="todo in list",反而增加嵌套层级。 -
工程实践建议: 对于简单组件(如 TodoList),使用多个
ref更清晰;对于大型状态管理,推荐reactive+defineExpose或结合 Pinia。
✅ 面试加分点:Vue3 的响应式系统基于
Proxy,ref本质是{ value: ... }的包装器,通过.value触发 getter/setter,从而收集依赖并触发更新。
三、computed:不只是“计算”,更是“缓存”与“双向绑定”
1. active 计算属性:性能优化的关键
const active = computed(() => todos.value.filter(t => !t.done).length)
-
为什么不用方法(method)?
- 方法每次渲染都会执行,而
computed具有缓存性:只有当todos变化时才重新计算。 - 在列表项多、频繁渲染的场景下,性能差异显著。
- 方法每次渲染都会执行,而
-
底层原理:
computed内部维护一个dirty标志位。首次访问时计算并缓存结果;当依赖的响应式数据(如todos)触发 setter 时,标记dirty = true,下次访问才重新计算。
🔍 延伸知识:这正是 “懒计算”(Lazy Evaluation) 的体现,避免不必要的 CPU 开销。
2. allDone:computed 的高级用法 —— 可写的计算属性
const allDone = computed({
get() { return todos.value.every(t => t.done) },
set(val) { todos.value.forEach(t => t.done = val) }
})
-
为什么能实现“全选/取消全选”?
- 当用户点击全选复选框,
v-model="allDone"会触发set函数,将所有todo.done设为val(true/false)。 - 这本质上是派生状态的反向同步,是 Vue 响应式系统的强大之处。
- 当用户点击全选复选框,
-
对比传统做法: 若不用
computed,需手动监听 checkbox 的 change 事件,再遍历数组赋值——代码冗余且易出错。
💡 设计模式关联:这体现了 Observer 模式(观察者模式)——状态变化自动通知视图更新。
四、v-model 与事件修饰符:语法糖背后的真相
<input v-model="title" @keydown.enter="addTodo" />
-
v-model是什么?- 本质是
:value="title" + @input="title = $event.target.value"的语法糖。 - 在 Vue3 中,
v-model默认监听update:modelValue事件,支持多v-model(如v-model:title)。
- 本质是
-
@keydown.enter的作用?- 监听回车键,避免用户必须点击“添加”按钮。
- 这是 事件委托 + 键盘事件处理 的典型应用,提升用户体验。
⚠️ 注意:
Math.random()作为id虽然方便,但在生产环境应使用更可靠的 ID 生成策略(如Date.now() + index或uuid),避免重复。
五、v-for 的 key:为什么必须唯一?
<li v-for="todo in todos" :key="todo.id">
-
key 的作用:
- 帮助 Vue 的 Diff 算法 识别节点身份,实现高效 DOM 更新。
- 若 key 不唯一或使用
index,在列表顺序变化时会导致状态错乱(如 checkbox 选中状态错位)。
-
底层机制: Vue3 的
patch函数会根据 key 构建旧/新节点映射表,仅移动/更新必要节点,而非暴力重建。
📌 面试高频题:“为什么不能用 index 作为 key?” —— 答案就在这里!
六、响应式原理深挖:从数据变化到 DOM 更新的完整链路
当你修改 todos.value 时,发生了什么?
- 触发 setter:
todos是ref,其.value被 Proxy 拦截。 - 依赖收集:在 render 阶段,
computed和模板中的todos已通过effect收集为依赖。 - 触发更新:setter 中调用
trigger,通知所有依赖的 effect 重新执行。 - 调度更新:Vue 将更新任务推入微任务队列(
Promise.then),合并多次修改,避免重复渲染。 - 重新 render:执行新的 render function,生成 VNode,与旧 VNode diff,最终 patch 到真实 DOM。
🔥 关联知识点:
- 事件循环(Event Loop) :Vue 的 nextTick 基于 microtask,确保 DOM 更新后执行回调。
- 内存管理:effect 会自动清理未使用的依赖,防止内存泄漏。
七、延伸思考:如何让这个 TodoList 更“大厂”?
- 持久化存储:使用
localStorage+watch监听todos变化自动保存。 - 防抖搜索:添加过滤输入框,结合
debounce优化性能。 - TypeScript 支持:定义
Todo接口,提升类型安全。 - 单元测试:用 Vitest 测试
addTodo、allDone逻辑。 - 性能监控:用
console.time或 Performance API 测量长列表渲染耗时。
结语:Vue 的哲学 —— “让开发者专注业务,而非胶水代码”
这个 TodoList,表面上是增删改查,实则浓缩了 Vue3 的核心思想:
- 响应式驱动:数据即视图,状态即真理。
- 声明式 UI:描述“是什么”,而非“怎么做”。
- 组合优于继承:Composition API 让逻辑复用更灵活。
当你能在面试中不仅写出功能,还能讲清 computed 的缓存机制、key 的 Diff 原理、响应式系统的依赖追踪——你就已经超越了 80% 的候选人。
最后送大家一句话:
“在 Vue 的世界里,DOM 是果,数据是因。抓住因,果自然成。”