Vue 3 实战:使用 Composition API 构建响应式任务管理组件

Vue 3 是一款渐进式 JavaScript 框架,适合开发单页应用、后台管理系统以及需要复杂交互的 Web 界面。

相比直接操作 DOM,Vue 更强调数据驱动视图。开发者只需要维护业务状态,组件便能够根据响应式数据自动更新界面。

本文使用 Vue 3 的 Composition API 实现一个任务管理组件,涵盖任务新增、状态筛选、关键词搜索、计算属性和组件样式管理。

一、Vue 3 Composition API 的核心设计

在 Vue 3 中,<script setup> 是单文件组件常用的编写方式。

它允许开发者直接声明响应式变量、计算属性和事件处理函数,无需通过传统的 data、methods 和 computed 配置项组织代码。

其中,几个常用 API 分别承担不同职责:

  • ref():创建响应式状态。
  • reactive():创建响应式对象。
  • computed():根据已有状态计算派生数据。
  • watch():监听状态变化并执行副作用。
  • onMounted():在组件挂载完成后执行初始化逻辑。

对于列表筛选、表单输入和任务统计等功能,合理组合 ref() 与 computed() 通常就能满足需求。

二、定义任务数据和响应式状态

首先创建一个 Vue 3 单文件组件 TaskManager.vue。

任务包含唯一标识、名称、优先级和完成状态。页面还需要维护搜索关键词、状态筛选条件及新增任务的输入值。

<script setup>
import { ref, computed } from 'vue'

const tasks = ref([
  {
    id: crypto.randomUUID(),
    title: '检查服务端接口状态',
    priority: 'high',
    completed: false
  },
  {
    id: crypto.randomUUID(),
    title: '优化前端资源加载',
    priority: 'normal',
    completed: true
  },
  {
    id: crypto.randomUUID(),
    title: '整理组件测试用例',
    priority: 'normal',
    completed: false
  }
])

const keyword = ref('')
const statusFilter = ref('all')
const newTaskTitle = ref('')
const newTaskPriority = ref('normal')
</script>

使用 ref() 包装数组后,Vue 会追踪数组内容变化。

当任务新增、删除或状态发生改变时,依赖该数组的模板和计算属性都会自动更新。

使用 crypto.randomUUID() 生成任务标识,可以避免依赖数组索引作为唯一 ID,从而减少列表重排时可能出现的组件复用问题。

三、通过 computed 实现搜索与状态筛选

任务管理界面通常需要同时支持关键词搜索和状态过滤。

不建议把过滤后的任务复制到另一个可修改数组中,因为这样容易产生两份需要同步维护的状态。

更合理的方式是保留原始任务集合,然后通过 computed() 派生当前需要展示的数据。

在同一个 <script setup> 中继续添加:

const filteredTasks = computed(() => {
  const searchText = keyword.value
    .trim()
    .toLocaleLowerCase('zh-CN')

  return tasks.value.filter(task => {
    const matchesKeyword = task.title
      .toLocaleLowerCase('zh-CN')
      .includes(searchText)

    const matchesStatus =
      statusFilter.value === 'all' ||
      (
        statusFilter.value === 'completed' &&
        task.completed
      ) ||
      (
        statusFilter.value === 'pending' &&
        !task.completed
      )

    return matchesKeyword && matchesStatus
  })
})

const statistics = computed(() => {
  const total = tasks.value.length

  const completed = tasks.value.filter(
    task => task.completed
  ).length

  return {
    total,
    completed,
    pending: total - completed
  }
})

filteredTasks 同时依赖任务数据、搜索关键词和状态筛选条件。

任何一个依赖发生改变,Vue 都会在需要时重新计算结果。

与普通方法调用不同,计算属性具有缓存机制。当依赖保持不变时,重复读取计算属性不会反复执行过滤逻辑。

四、实现新增任务和状态更新

任务管理组件还需要支持新增任务和切换完成状态。

继续定义两个事件处理函数:

function addTask() {
  const title = newTaskTitle.value.trim()

  if (!title) {
    return
  }

  tasks.value.push({
    id: crypto.randomUUID(),
    title,
    priority: newTaskPriority.value,
    completed: false
  })

  newTaskTitle.value = ''
}

function toggleTask(taskId) {
  const task = tasks.value.find(
    item => item.id === taskId
  )

  if (!task) {
    return
  }

  task.completed = !task.completed
}

新增任务时,先清理输入内容,避免空字符串进入任务集合。

状态更新时,通过任务 ID 定位对象,而不是依赖当前列表的位置。这一点对排序、分页和筛选后的列表尤其重要。

Vue 3 能够追踪响应式对象的属性变化,因此修改 task.completed 后,无需手动调用 DOM 更新函数。

五、使用 Vue 模板实现交互界面

业务逻辑定义完成后,可以利用 Vue 的模板指令绑定用户操作。

这里主要使用:

  • v-model 实现表单双向绑定。
  • v-for 渲染任务列表。
  • v-if 控制条件展示。
  • :key 标识列表中的独立任务。
  • @click 和 @submit.prevent 处理交互事件。

在组件中加入以下模板:

<template>
  <main class="task-manager">
    <header class="page-header">
      <h1>Vue 3 任务管理系统</h1>
      <p>基于 Composition API 的响应式任务管理</p>
    </header>

    <section class="statistics">
      <div>
        <strong>{{ statistics.total }}</strong>
        <span>全部任务</span>
      </div>

      <div>
        <strong>{{ statistics.pending }}</strong>
        <span>待处理</span>
      </div>

      <div>
        <strong>{{ statistics.completed }}</strong>
        <span>已完成</span>
      </div>
    </section>

    <section class="task-controls">
      <input
        id="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/757adc6eda21b34988fb01a0c1384312980738cd"
        v-model="keyword"
        type="search"
        placeholder="搜索任务名称"
        aria-label="搜索任务"
      />

      <select
        class="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/c6e274d891618a2d95ae997690ff336ed1392479"
        v-model="statusFilter"
        aria-label="筛选任务状态"
      >
        <option value="all">全部任务</option>
        <option value="pending">待处理</option>
        <option value="completed">已完成</option>
      </select>
    </section>

    <form
      class="task-form"
      @submit.prevent="addTask"
    >
      <input
        id="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/c99b922b6d1a3230631d97a08b6212cc41daae85"
        v-model="newTaskTitle"
        type="text"
        placeholder="输入新的任务名称"
        aria-label="新任务名称"
      />

      <select
        v-model="newTaskPriority"
        aria-label="新任务优先级"
      >
        <option value="normal">普通优先级</option>
        <option value="high">高优先级</option>
      </select>

      <button
        class="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/e0bde5ff42b9641e090f7992217cc37e441c6407"
        type="submit"
        :disabled="!newTaskTitle.trim()"
      >
        添加任务
      </button>
    </form>

    <section class="task-results">
      <h2>任务列表</h2>

      <ul
        id="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/5e7f2dbf3888198afc126ea5ab444acb35dd7937"
        class="task-list"
      >
        <li
          v-for="task in filteredTasks"
          :key="task.id"
          :class="{ completed: task.completed }"
        >
          <label>
            <input
              type="checkbox"
              :checked="task.completed"
              @change="toggleTask(task.id)"
            />

            <span>{{ task.title }}</span>
          </label>

          <span
            class="priority"
            :data-priority="task.priority"
          >
            {{ task.priority === 'high' ? '高优先级' : '普通' }}
          </span>
        </li>
      </ul>

      <p
        v-if="filteredTasks.length === 0"
        class="empty-state"
      >
        暂无符合条件的任务
      </p>
    </section>
  </main>
</template>

在这套模板中,用户输入内容会自动同步到对应的响应式变量。

例如,搜索框的 v-model="keyword" 会使输入变化直接反映到 keyword,继而更新 filteredTasks。

列表渲染使用任务 ID 作为 key,避免因列表顺序变化而产生不必要的 DOM 状态混乱。

六、使用 scoped CSS 管理组件样式

Vue 单文件组件可以通过 <style scoped> 限制样式的作用范围。

这样能够降低不同业务组件之间发生样式冲突的概率。

<style scoped>
.task-manager {
  max-width: 920px;
  margin: 40px auto;
  padding: 24px;
  font-family: system-ui, sans-serif;
}

.page-header {
  margin-bottom: 24px;
}

.page-header h1 {
  margin-bottom: 8px;
}

.page-header p {
  color: #64748b;
}

.statistics {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-bottom: 24px;
}

.statistics > div {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 18px;
  border: 1px solid #e2e8f0;
  border-radius: 10px;
}

.statistics strong {
  font-size: 28px;
}

.task-controls,
.task-form {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
}

input,
select,
button {
  padding: 10px 12px;
  border: 1px solid #cbd5e1;
  border-radius: 6px;
  font: inherit;
}

.task-controls input,
.task-form input {
  flex: 1;
  min-width: 0;
}

button {
  background: #2563eb;
  color: white;
  cursor: pointer;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.task-list {
  padding: 0;
  list-style: none;
}

.task-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding: 14px;
  margin-bottom: 10px;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
}

.task-list li.completed label span {
  text-decoration: line-through;
  color: #94a3b8;
}

.priority[data-priority="high"] {
  color: #dc2626;
}

.empty-state {
  padding: 32px;
  text-align: center;
  color: #64748b;
}

@media (max-width: 640px) {
  .statistics {
    grid-template-columns: 1fr;
  }

  .task-controls,
  .task-form {
    flex-direction: column;
  }
}
</style>

scoped 的实现依赖 Vue 编译器为组件元素和样式选择器添加作用域标识,因此它并不等同于浏览器原生的 Shadow DOM。

对于大部分中小型业务组件,scoped 能够提供足够清晰的样式隔离机制。

七、Vue 响应式系统与 DOM 操作的关系

在传统 JavaScript 开发中,开发者通常需要手动执行以下操作:

  1. 监听用户输入事件。
  2. 查询对应的 DOM 元素。
  3. 根据输入条件过滤数据。
  4. 清空原有列表。
  5. 重新创建并插入 DOM 节点。

而在 Vue 3 中,状态与视图之间建立了响应式依赖关系。

例如:

const count = ref(0)

const doubled = computed(() => count.value * 2)

count.value++

当 count 改变时,doubled 会在下次访问时反映新的值。如果模板使用了 doubled,Vue 也会在更新周期中刷新对应的界面内容。

开发者无需直接修改文本节点。

需要注意,Vue 的 DOM 更新采用批量调度机制。响应式状态更新后,实际 DOM 不一定立即完成更新。

如果业务逻辑必须在 DOM 更新后读取布局信息,可以使用 nextTick()。

import { nextTick } from 'vue'

async function updateAndMeasure() {
  count.value++

  await nextTick()

  // 此时可以读取更新后的 DOM
}

这种机制既有利于减少重复渲染,也要求开发者区分状态变化与 DOM 更新完成这两个时间点。

八、任务数量较大时如何优化

当前示例适合任务量相对有限的管理界面。

当任务数量增加到数千甚至数万条时,应该重点考虑以下优化方向。

1. 使用分页或虚拟列表

如果一次性渲染大量任务节点,即使 Vue 的响应式更新效率较高,浏览器仍然需要承担较大的布局和绘制成本。

分页适合传统业务后台,虚拟列表则适合需要连续滚动的大数据量界面。

2. 将复杂筛选迁移到服务端

当任务数据来自后端数据库时,不应该无限制地把所有记录加载到浏览器。

可以通过接口提交筛选条件:

const params = new URLSearchParams({
  status: statusFilter.value,
  keyword: keyword.value
})

const response = await fetch(`/api/tasks?${params}`)

if (!response.ok) {
  throw new Error(`请求失败:${response.status}`)
}

const result = await response.json()

服务端负责过滤、排序和分页,前端主要负责维护查询状态及展示结果。

实际项目还需要根据接口契约处理响应结构、请求取消和错误状态。

3. 避免不必要的响应式计算

如果某些数据不会发生变化,可以避免将其放入深层响应式结构中。

对于体积较大、内部不需要递归追踪的对象,还可以考虑 shallowRef()。

但不应在缺少性能证据时提前使用复杂优化方案。

4. 对异步请求进行竞争控制

当用户快速切换筛选条件时,多个异步请求可能同时进行。

较早发出的请求不一定较早返回。

使用 AbortController 或请求序号控制,可以避免过期响应覆盖最新查询结果。

九、总结

Vue 3 的核心优势在于通过响应式状态组织界面逻辑,让开发者从繁琐的 DOM 更新中抽离出来。

在任务管理这类典型业务场景中,ref() 负责维护状态,computed() 负责派生数据,v-model 负责表单同步,v-for 负责列表渲染,而 <script setup> 则让这些逻辑能够集中、清晰地组织在一个组件中。

随着项目复杂度增加,可以逐步引入组件拆分、Pinia 状态管理、Vue Router、接口数据缓存和虚拟列表等能力。

对于中小型前端项目,优先建立清晰的数据流和组件边界,通常比过早引入复杂架构更有价值。

posted @ 2026-11-12 17:40  YXTK招募助手  阅读(4)  评论(0)    收藏  举报