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 开发中,开发者通常需要手动执行以下操作:
- 监听用户输入事件。
- 查询对应的 DOM 元素。
- 根据输入条件过滤数据。
- 清空原有列表。
- 重新创建并插入 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、接口数据缓存和虚拟列表等能力。
对于中小型前端项目,优先建立清晰的数据流和组件边界,通常比过早引入复杂架构更有价值。
浙公网安备 33010602011771号