跳到主要内容

3.8 组合式函数与逻辑复用

本章目标: 把任务状态和操作从组件提取到函数中,保留响应式能力,并明确共享范围。

useXxx 是一种约定​

组合式函数使用 Vue 响应式 API 封装有状态的逻辑,通常以 use 开头。纯字符串格式化函数无需叫 useFormatTitle,普通工具函数就够。

src/composables/useTasks.ts
import { computed, ref } from 'vue';
import type { TaskItem } from '../types/task';

export function useTasks() {
const tasks = ref<TaskItem[]>([]);
let nextId = 1;
const pendingCount = computed(() => tasks.value.filter(t => !t.isDone).length);

function add(title: string) {
const value = title.trim();
if (!value) return false;
tasks.value.push({ id: nextId++, title: value, isDone: false });
return true;
}

function toggle(id: number) {
const task = tasks.value.find(t => t.id === id);
if (task) task.isDone = !task.isDone;
}

function remove(id: number) {
tasks.value = tasks.value.filter(t => t.id !== id);
}

return { tasks, pendingCount, add, toggle, remove };
}

先创建组件章节中的 src/types/task.ts。这个本地版本只用于学习,标题上限等完整校验在实战项目中处理。

组件使用返回的 ref​

src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { useTasks } from './composables/useTasks';

const title = ref('');
const { tasks, pendingCount, add, toggle, remove } = useTasks();

function submit() {
if (add(title.value)) title.value = '';
}
</script>

<template>
<main>
<form @submit.prevent="submit">
<input v-model="title" aria-label="任务名称" />
<button type="submit">添加</button>
</form>
<p>待完成 {{ pendingCount }} 项</p>
<ul>
<li v-for="task in tasks" :key="task.id">
<button type="button" @click="toggle(task.id)">
{{ task.isDone ? '恢复' : '完成' }}
</button>
{{ task.title }}
<button type="button" @click="remove(task.id)">删除</button>
</li>
</ul>
</main>
</template>

函数返回普通对象,内部字段是 ref,所以解构不会丢失关联。它与直接解构 reactive 对象的原始值有区别。

每次调用创建独立状态​

const first = useTasks();
const second = useTasks();
first.add('第一组任务');
console.log(second.tasks.value.length); // 0

状态放在函数内部,每次调用都重新创建。放到模块顶层则会共享给使用该模块的调用者;在 SSR 场景下还可能造成跨请求数据泄漏。

对照服务生命周期

不能从 useTasks 这个名字判断它是单例还是瞬态。必须查看状态创建的位置。跨页面共享状态通常用 Pinia,并按应用实例管理。

有资源的逻辑仍需清理​

如果组合式函数内部使用 onMounted / onUnmounted,通常要在组件 setup 同步阶段调用它。定时器、监听器和请求清理仍需明确注册。

分层建议:组件负责展示与交互;组合式函数协调状态;API 模块负责传输与返回值验证。不要为了分层给每个单行操作都加一个封装。

练习​

给 useTasks 增加 clearCompleted(),保留未完成任务。在两个不同组件分别调用 useTasks,验证它们的状态不会自动共享。

参考答案
function clearCompleted() {
tasks.value = tasks.value.filter(t => !t.isDone);
}

把函数加入返回对象。需要同一组数据时,让共同父组件创建状态并传递,或学习下一阶段的 Pinia。

参考:组合式函数。