别再手动操作 DOM 了!Vue3 CompositionAPI 写 TodoList,面试官看了直呼内行

63 阅读5分钟

别再手动操作 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 包裹整个状态对象?

深度解析:

  • ref vs reactive

    • 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 的响应式系统基于 Proxyref 本质是 { 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() + indexuuid),避免重复。


五、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 时,发生了什么?

  1. 触发 settertodosref,其 .value 被 Proxy 拦截。
  2. 依赖收集:在 render 阶段,computed 和模板中的 todos 已通过 effect 收集为依赖。
  3. 触发更新:setter 中调用 trigger,通知所有依赖的 effect 重新执行。
  4. 调度更新:Vue 将更新任务推入微任务队列(Promise.then),合并多次修改,避免重复渲染。
  5. 重新 render:执行新的 render function,生成 VNode,与旧 VNode diff,最终 patch 到真实 DOM。

🔥 关联知识点

  • 事件循环(Event Loop) :Vue 的 nextTick 基于 microtask,确保 DOM 更新后执行回调。
  • 内存管理:effect 会自动清理未使用的依赖,防止内存泄漏。

七、延伸思考:如何让这个 TodoList 更“大厂”?

  1. 持久化存储:使用 localStorage + watch 监听 todos 变化自动保存。
  2. 防抖搜索:添加过滤输入框,结合 debounce 优化性能。
  3. TypeScript 支持:定义 Todo 接口,提升类型安全。
  4. 单元测试:用 Vitest 测试 addTodoallDone 逻辑。
  5. 性能监控:用 console.time 或 Performance API 测量长列表渲染耗时。

结语:Vue 的哲学 —— “让开发者专注业务,而非胶水代码”

这个 TodoList,表面上是增删改查,实则浓缩了 Vue3 的核心思想:

  • 响应式驱动:数据即视图,状态即真理。
  • 声明式 UI:描述“是什么”,而非“怎么做”。
  • 组合优于继承:Composition API 让逻辑复用更灵活。

当你能在面试中不仅写出功能,还能讲清 computed 的缓存机制、key 的 Diff 原理、响应式系统的依赖追踪——你就已经超越了 80% 的候选人。

最后送大家一句话:
“在 Vue 的世界里,DOM 是果,数据是因。抓住因,果自然成。”