Vue 3 Composition API 实战:打造一个响应式 Todo 清单应用

40 阅读6分钟

🧠 一、核心思想:从“操作 DOM”到“操作数据”

❌ 传统做法(命令式)

js
编辑
// 找到输入框、按钮、列表
const input = document.querySelector('#input');
const btn = document.querySelector('#add-btn');
const list = document.querySelector('#todo-list');

btn.addEventListener('click', () => {
  const li = document.createElement('li');
  li.textContent = input.value;
  list.appendChild(li);
  input.value = '';
});
  • 问题:代码耦合度高,逻辑分散,难以维护。
  • 思维模式:先找元素 → 再改内容 → 手动同步状态。

✅ Vue 做法(声明式 + 响应式)

“你只管改数据,DOM 更新我来搞定。”

在 Vue 中,我们不再关心“如何更新页面”,而是专注思考:

  • 数据结构是什么?
  • 用户交互会如何改变数据?

框架自动追踪依赖,在数据变化时高效更新视图。


🧩 二、代码逐层解析

1. 模板部分(Template)

vue
编辑
<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" />
      {{ active }} / {{ todos.length }}
    </div>
  </div>
</template>

🔑 关键指令说明:

指令作用简写/说明
{{ }}插值表达式动态显示数据
v-model双向数据绑定自动同步 input 与 data
@keydown.enter事件监听(回车)@ 是 v-on: 的缩写
v-if / v-else条件渲染控制元素是否渲染
v-for列表渲染遍历数组生成元素
:key唯一标识提升 diff 效率,必加!
:class动态绑定 class: 是 v-bind: 的缩写

💡 :class="{ done: todo.done }"
todo.done === true 时,<span> 会加上 done 类,触发 CSS 样式(灰色+删除线)。


🔍 三、深度辨析:v-bind(:) vs v-on(@)

你提到的「:」其实是 Vue 中 v-bind 指令的简写,而 @v-on 指令的简写。虽然都叫“绑定”,但二者绑定的目标、作用、场景完全不同

下面从多个维度详细对比,帮你彻底区分:

一、核心定位对比

维度v-bind(简写 :v-on(简写 @
绑定目标DOM 属性(如 src/class/style)、组件 props、自定义属性DOM 事件(如 click/input)、组件自定义事件
作用「数据 → 视图」:将 Vue 数据同步到 DOM / 组件「视图 → 数据」:监听视图事件,触发逻辑修改数据
本质设置元素 / 组件的「状态 / 属性」给元素 / 组件添加「事件监听器」
取值类型表达式(变量、计算属性、对象、数组等)方法名、内联表达式、事件处理函数

二、具体用法对比

1. v-bind:):绑定「属性 / Props」

核心是 把 Vue 实例的数据,赋值给 DOM 元素的属性或组件的 props

示例 1:绑定原生 DOM 属性

vue
编辑
<!-- 完整写法 -->
<img v-bind:src="imgUrl" alt="">

<!-- 简写 -->
<img :src="imgUrl" alt="">

<!-- 绑定 class/style(支持对象/数组语法) -->
<div :class="{ active: isActive }" :style="{ color: textColor }"></div>

<!-- 绑定自定义属性(Vue 3 中可直接用 :data-id) -->
<div :data-id="itemId"></div>
  • imgUrlisActivetextColor 是 Vue 响应式数据;
  • 数据变化时,DOM 属性自动更新(响应式同步)。

示例 2:绑定组件 Props

vue
编辑
<!-- 父组件:向子组件传递数据 -->
<Child :name="username" :age="userAge"></Child>

<!-- 子组件:接收 props -->
<script setup>
defineProps(['name', 'age'])
</script>
  • v-bind 是父→子通信的唯一标准方式(Props 单向数据流)。

2. v-on@):绑定「事件」

核心是 给 DOM 元素 / 组件绑定事件监听器,事件触发时执行指定逻辑

示例 1:绑定原生 DOM 事件

vue
编辑
<!-- 完整写法 -->
<button v-on:click="handleClick">点击</button>

<!-- 简写 -->
<button @click="handleClick">点击</button>

<!-- 键盘事件 -->
<input @keyup.enter="handleSearch">
  • handleClickhandleSearch 是方法;
  • 用户交互(点击、回车)触发后,可修改数据,进而驱动视图更新。

示例 2:绑定组件自定义事件

vue
编辑
<!-- 子组件:触发事件 -->
<button @click="$emit('confirm', '子组件数据')">确认</button>

<!-- 父组件:监听事件 -->
<Child @confirm="handleChildConfirm"></Child>
  • v-on 是实现子→父通信的关键机制。

三、关键区别:「数据绑定」vs「事件绑定」

对比项v-bind(:)v-on(@)
数据流向数据 → 视图(单向)视图 → 数据(通过事件回调)
触发时机初始化 + 数据变化时自动执行事件发生时被动触发
典型修饰符.prop.camel(Vue 2 还有 .sync.stop.prevent.once.enter 等

🌟 特别注意
:value="inputVal" 不会自动同步用户输入回 inputVal
@input="inputVal = $event.target.value" 才能实现反向同步。


四、易混场景:v-model(结合了两者)

v-model 本质是 语法糖,内部同时使用了 v-bindv-on

vue
编辑
<!-- 简写 -->
<input v-model="inputVal">

<!-- 等价于 -->
<input 
  :value="inputVal" 
  @input="inputVal = $event.target.value"
>

这完美体现了:

  • :v-bind)负责 把数据给视图(设置 value);
  • @v-on)负责 把视图的变化同步回数据(监听 input 事件)。

五、一句话总结使用场景

场景用 v-bind(:)用 v-on(@)
给 <img> 绑定 src
给 <div> 绑定 class/style
父组件给子组件传值
点击按钮执行逻辑
监听输入框内容变化
父组件监听子组件事件

口诀

  • **冒号(:)绑属性,@ 绑事件;

    是数据到视图,@ 是视图触发数据改。**


🔥 四、深度解析:“全选”功能是如何实现的?

要彻底搞懂全选功能,我们必须理解 可读写计算属性 allDone复选框 v-model 的联动机制。下面从三个维度拆解:

一、先看核心代码(回顾)

html
预览
<!-- 模板:全选复选框 -->
全选 <input type="checkbox" v-model="allDone">
js
编辑
// 脚本:可读写计算属性 allDone
const allDone = computed({
  get() {
    return todos.value.every(todo => todo.done);
  },
  set(value) {
    todos.value.forEach(todo => todo.done = value);
  }
});

二、全勾选的实现逻辑(分 2 种场景)

场景 1️⃣:手动勾选 / 取消 “全选框” → 所有待办被勾选 / 取消

这是正向联动,由用户主动操作全选框触发:

  1. 用户点击“全选”复选框 → 浏览器将 checked 设为 true 或 false

  2. v-model="allDone" 将这个布尔值赋值给 allDone

    • 底层v-model 展开为 :checked="allDone" + @change="allDone = $event.target.checked"
    • 其中 :checked 是 v-bind@change 是 v-on
  3. 由于 allDone 是可写的计算属性,赋值操作触发 set(value) 方法

  4. set 方法遍历 todos,将每个 todo.done 设置为 value

  5. 每个待办项的复选框通过 v-model="todo.done"(即 :checked + @change)绑定,因此会自动同步状态

本质v-on 触发 set → 批量修改数据 → v-bind 更新子项视图。

场景 2️⃣:手动勾选完所有单个待办 → 全选框自动勾选

这是反向联动,由子项变化驱动父控件:

  1. 用户逐个勾选待办项 → 对应的 todo.done 变为 true

  2. todos 是 ref 响应式数据,其内部变化会被 Vue 追踪;

  3. allDone 的 get() 方法依赖于 todos,因此会自动重新执行

  4. get() 使用 Array.prototype.every() 检查:是否所有 todo.done === true

    • every():只有全部满足条件才返回 true,否则 false
  5. 若返回 true,则 allDone 的值变为 true

  6. 全选框的 :checked="allDone"(来自 v-model)读取该值 → 自动勾选

本质:子项数据变 → 触发 get 重算 → v-bind 更新全选框状态。


三、核心关键点总结

核心环节作用
v-model="allDone"(全选框)展开为 :checked="allDone"v-bind) + @change="..."v-on
computed 的 get()实时检查所有待办是否完成,返回“是否全选”的布尔值
computed 的 set(value)接收全选框的新状态,批量设置所有 todo.done = value
v-model="todo.done"(单个框)同步单个待办的勾选状态 ↔ 数据字段
响应式数据(ref确保 todos 和 allDone 的变化能触发视图更新

四、举个具体例子(更容易理解)

假设初始状态:

js
编辑
todos.value = [
  { id:1, title:'打王者', done:true },
  { id:2, title:'吃饭', done:true }
];
  • 页面加载时allDone.get() 执行 → every() 返回 true → 全选框通过 :checked="true" 勾选 ✅
  • 你取消“打王者”todo.done = false → todos 变化 → allDone.get() 重算 → 返回 false → 全选框通过 :checked="false" 取消 ❌
  • 你点击全选框勾选@change 触发 → allDone = true → 触发 set(true) → 所有 todo.done = true → 子项复选框通过 :checked 更新 ✅

整个过程完全由数据驱动,且清晰分离了 属性绑定(:事件绑定(@ 的职责。


五、为什么不用普通变量 / 方法实现?

如果放弃 computed 的可读写特性,改用普通方法,代码会变得冗长且难以维护:

js
编辑
// ❌ 普通方式(繁琐)
const allDone = ref(false);

watch(todos, () => {
  allDone.value = todos.value.every(todo => todo.done);
});

const toggleAll = (value) => {
  todos.value.forEach(todo => todo.done = value);
};

而使用 可读写 computed

  • 自动响应依赖变化(无需 watch
  • 读写逻辑封装一体get/set
  • 模板直接 v-model 绑定(简洁直观)

这就是 Vue 计算属性的高级威力:把复杂的状态映射逻辑,变成一个“看起来像普通变量”的响应式值。


📌 五、总结要点

概念说明
响应式核心数据变 → 视图自动更新,开发者专注数据流
Composition APIref + computed + 函数组合,逻辑更清晰
v-bind(:)绑定属性/props,实现 数据 → 视图
v-on(@)绑定事件,实现 视图 → 数据
v-model是 v-bind + v-on 的语法糖,实现双向绑定
computed 缓存机制避免重复计算,提升性能
可读写 computed实现“全选”这类双向状态映射的最佳实践
Array.every()判断数组是否全部满足条件,是实现“全选检测”的关键 JS 方法