【Vue3 组合式函数】+【前端开发实战】:从命名、输入输出到复用边界,彻底搞懂组合式函数的最佳封装写法,避开响应式丢失、副作用未清理等高频坑!

📑 文章目录
- 一、前言:为什么要单独讲「组合式函数」?
- 二、组合式函数到底是什么?
- 三、命名规范:一看就知道用途
- [3.1 函数命名:use + 动词/名词](#3.1 函数命名:use + 动词/名词)
- [3.2 返回值命名:结构清晰、语义明确](#3.2 返回值命名:结构清晰、语义明确)
- [3.3 参数命名:一看就懂用法](#3.3 参数命名:一看就懂用法)
- 四、输入输出设计:边界要清晰
- [4.1 输入:尽量用「配置对象」而不是「一长串参数」](#4.1 输入:尽量用「配置对象」而不是「一长串参数」)
- [4.2 输出:只暴露必要内容](#4.2 输出:只暴露必要内容)
- [4.3 完整示例:useRequest 的输入输出设计](#4.3 完整示例:useRequest 的输入输出设计)
- 五、复用边界:什么时候该抽、抽成什么
- [5.1 判断标准:3 次原则](#5.1 判断标准:3 次原则)
- [5.2 常见边界划分](#5.2 常见边界划分)
- [5.3 反例:把业务写死在 Hook 里](#5.3 反例:把业务写死在 Hook 里)
- [5.4 完整示例:useCountdown 的边界](#5.4 完整示例:useCountdown 的边界)
- [六、与 Vue 组件、模板的配合规范](#六、与 Vue 组件、模板的配合规范)
- [6.1 组件只负责「组合」和「视图」](#6.1 组件只负责「组合」和「视图」)
- [6.2 模板里避免复杂逻辑](#6.2 模板里避免复杂逻辑)
- [6.3 组件 props 与 Hooks 的配合](#6.3 组件 props 与 Hooks 的配合)
- 七、常见坑与避坑指南
- [7.1 忘记处理响应式](#7.1 忘记处理响应式)
- [7.2 生命周期和副作用没清理](#7.2 生命周期和副作用没清理)
- [7.3 把组合式函数当工具函数用](#7.3 把组合式函数当工具函数用)
- 八、小结:一套可直接照搬的检查清单
- [🔍 系列模块导航](#🔍 系列模块导航)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、前言:为什么要单独讲「组合式函数」?
如果你已经能写 Vue 组件,但总觉得:
- 抽逻辑时不知道往哪儿放
- 自己写的
useXxx别人难复用 - 复用逻辑时容易和业务强耦合
多半是组合式函数(Composition API / Hooks)的边界没理清。
这篇文章不讲底层原理,只讲:日常写代码该怎么选、为什么这么选、坑会出在哪儿。目标是帮你形成一套可落地的封装规范,并在业务里持续复用。
[⬆ 返回目录](#⬆ 返回目录)
二、组合式函数到底是什么?
简单说:把可复用的响应式逻辑抽到一个函数里,这个函数就叫组合式函数。
它必须:
- 以
use开头命名(约定俗成) - 只处理逻辑,不渲染模板
- 可以被多个组件重复调用
组合式函数 ≠ 组件,它是"逻辑单元",不是"视图单元"。
[⬆ 返回目录](#⬆ 返回目录)
三、命名规范:一看就知道用途
3.1 函数命名:use + 动词/名词
javascript
// ✅ 好的命名:动词表示行为
useToggle() // 切换状态
useRequest() // 发请求
useLocalStorage() // 读写本地存储
useDebounce() // 防抖
// ✅ 好的命名:名词表示能力
useCountdown() // 倒计时
usePagination() // 分页
useTableSelection()// 表格选中
// ❌ 不好的命名:含义模糊
useData() // 什么数据?干什么用?
useHandle() // 处理什么?
useLogic() // 逻辑太泛
原则:从名字就能猜出功能和职责。
[⬆ 返回目录](#⬆ 返回目录)
3.2 返回值命名:结构清晰、语义明确
javascript
// ✅ 推荐:对象解构,名字和用途一一对应
function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
// 使用时:const { count, increment, decrement } = useCounter()
// ✅ 也可以:数组返回,适合顺序固定、数量少的场景
function useToggle(initial = false) {
const state = ref(initial)
const toggle = () => { state.value = !state.value }
return [state, toggle]
}
// 使用时:const [isVisible, toggleVisible] = useToggle()
尽量避免:返回值里既有数据又有方法,名字却很抽象(如 data、methods),别人很难猜到怎么用。
[⬆ 返回目录](#⬆ 返回目录)
3.3 参数命名:一看就懂用法
javascript
// ✅ 好:参数有默认值、含义清晰
function useRequest(url, options = {}) {
const { immediate = true, method = 'GET' } = options
// ...
}
// ✅ 好:用对象收拢可选参数
function usePagination({
pageSize = 10,
total = 0,
currentPage = 1
} = {}) {
// ...
}
// ❌ 差:参数多且无默认值,调用方容易传错
function useRequest(url, immediate, method, headers, timeout) { }
[⬆ 返回目录](#⬆ 返回目录)
四、输入输出设计:边界要清晰
4.1 输入:尽量用「配置对象」而不是「一长串参数」
javascript
// ❌ 不推荐:参数一多就很难记顺序
function useTable(config, pageSize, currentPage, sortField, sortOrder) { }
// ✅ 推荐:用对象 + 默认值
function useTable(options = {}) {
const {
pageSize = 10,
currentPage = 1,
sortField = 'id',
sortOrder = 'asc'
} = options
// ...
}
这样既能扩展参数,又不会破坏现有调用。
[⬆ 返回目录](#⬆ 返回目录)
4.2 输出:只暴露必要内容
javascript
// ❌ 不好:把内部细节也暴露了
function useUserList() {
const loading = ref(false)
const error = ref(null)
const internalCache = ref({}) // 内部缓存,不该暴露
const _fetchData = () => {} // 内部方法,不该暴露
return { loading, error, internalCache, _fetchData }
}
// ✅ 好:只暴露「用户需要」的
function useUserList() {
const loading = ref(false)
const error = ref(null)
const users = ref([])
const refetch = async () => { /* ... */ }
return { loading, error, users, refetch }
原则:只返回外部会用到的状态和方法,内部实现细节一律不暴露。
[⬆ 返回目录](#⬆ 返回目录)
4.3 完整示例:useRequest 的输入输出设计
javascript
// useRequest.js
import { ref, watch } from 'vue'
/**
* 通用请求 Hook
* @param {string} url - 请求地址
* @param {Object} options - 配置项
* @param {boolean} options.immediate - 是否立即请求,默认 true
* @param {Object} options.params - 请求参数,变化时自动重新请求
*/
export function useRequest(url, options = {}) {
const { immediate = true, params = {} } = options
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const execute = async () => {
loading.value = true
error.value = null
try {
const res = await fetch(`${url}?${new URLSearchParams(params)}`)
data.value = await res.json()
return data.value
} catch (e) {
error.value = e
throw e
} finally {
loading.value = false
}
}
// params 变化时重新请求
watch(() => ({ ...params }), execute, { immediate })
return { data, loading, error, execute }
javascript
// 组件中使用
import { useRequest } from '@/hooks/useRequest'
export default {
setup() {
const { data, loading, error, execute } = useRequest('/api/users', {
params: { page: 1, size: 10 },
immediate: true
})
return { data, loading, error, execute }
}
}
要点:
- 输入:
url+options,语义清晰 - 输出:
data、loading、error、execute,都是外部会用到的 - 内部逻辑完全封装在函数内
[⬆ 返回目录](#⬆ 返回目录)
五、复用边界:什么时候该抽、抽成什么
5.1 判断标准:3 次原则
- 只用了 1 次:直接在组件里写
- 用了 2 次:可以先复制粘贴,确认会长期复用再抽
- 用了 3 次及以上:建议抽成组合式函数
不要为了"显得高级"而过早抽象。
[⬆ 返回目录](#⬆ 返回目录)
5.2 常见边界划分
| 场景 | 适合放哪 | 理由 |
|---|---|---|
| 纯 UI 状态(如弹窗开关) | 组件内 | 和当前组件强相关 |
| 跨组件复用的业务逻辑 | 组合式函数 | 逻辑可复用,视图各自定义 |
| 纯工具函数(无响应式) | 普通工具函数 | 不是 Hook,不必 use 开头 |
| 全局状态 | Pinia/Vuex | 跨很多组件共享 |
[⬆ 返回目录](#⬆ 返回目录)
5.3 反例:把业务写死在 Hook 里
javascript
// ❌ 错误:Hook 里写死了「用户列表」业务
function useUserList() {
const users = ref([])
const fetchUsers = async () => {
const res = await fetch('/api/users') // 写死接口
users.value = await res.json()
}
return { users, fetchUsers }
问题:换接口、换业务就要改 Hook,复用性差。
javascript
// ✅ 正确:Hook 只负责「请求流程」,业务由调用方决定
function useRequest(url, options = {}) {
const data = ref(null)
const loading = ref(false)
const execute = async () => {
loading.value = true
try {
const res = await fetch(url)
data.value = await res.json()
} finally {
loading.value = false
}
}
return { data, loading, execute }
}
// 使用时传入不同 url 即可
const { data: users, loading, execute } = useRequest('/api/users')
const { data: products, loading: loadingProducts } = useRequest('/api/products')
[⬆ 返回目录](#⬆ 返回目录)
5.4 完整示例:useCountdown 的边界
javascript
// useCountdown.js
import { ref, onUnmounted } from 'vue'
/**
* 倒计时 Hook
* 职责:倒计时逻辑(开始、暂停、重置)
* 不职责:显示样式、文案
*/
export function useCountdown(initialSeconds = 60) {
const remaining = ref(initialSeconds)
const isRunning = ref(false)
let timer = null
const start = () => {
if (isRunning.value) return
isRunning.value = true
timer = setInterval(() => {
remaining.value--
if (remaining.value <= 0) {
stop()
}
}, 1000)
}
const stop = () => {
clearInterval(timer)
timer = null
isRunning.value = false
}
const reset = () => {
stop()
remaining.value = initialSeconds
}
onUnmounted(stop)
return { remaining, isRunning, start, stop, reset }
html
<!-- 验证码倒计时组件 -->
<template>
<button
:disabled="isRunning"
@click="handleSend"
>
{{ isRunning ? `${remaining}秒后重试` : '发送验证码' }}
</button>
</template>
<script setup>
import { useCountdown } from '@/hooks/useCountdown'
const { remaining, isRunning, start, reset } = useCountdown(60)
const handleSend = async () => {
await sendSmsApi() // 业务接口
reset()
start()
}
</script>
要点:
- Hook 只负责"倒计时状态与行为"
- 文案、样式、业务接口都在组件里
[⬆ 返回目录](#⬆ 返回目录)
六、与 Vue 组件、模板的配合规范
6.1 组件只负责「组合」和「视图」
html
<!-- UserList.vue -->
<template>
<div class="user-list">
<div v-if="loading">加载中...</div>
<div v-else-if="error">加载失败</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="refetch">刷新</button>
</div>
</template>
<script setup>
// 组件 = 组合多个 Hook + 定义模板
import { useRequest } from '@/hooks/useRequest'
const { data: users, loading, error, execute: refetch } = useRequest('/api/users')
</script>
组件 = 组合式函数 + 模板,职责清晰。
[⬆ 返回目录](#⬆ 返回目录)
6.2 模板里避免复杂逻辑
html
<!-- ❌ 不好:模板里一堆计算 -->
<template>
<span>{{ items.filter(i => i.checked).length }} / {{ items.length }}</span>
</template>
<!-- ✅ 好:用 computed 抽出来 -->
<template>
<span>{{ checkedCount }} / {{ totalCount }}</span>
</template>
<script setup>
const checkedCount = computed(() => items.value.filter(i => i.checked).length)
const totalCount = computed(() => items.value.length)
</script>
[⬆ 返回目录](#⬆ 返回目录)
6.3 组件 props 与 Hooks 的配合
javascript
// 当 Hook 需要依赖组件 props 时,用 watch/watchEffect 或传参
function useUserDetail(userId) {
const user = ref(null)
watch(() => userId, async (id) => {
if (!id) return
user.value = await fetchUser(id)
}, { immediate: true })
return { user }
}
// 组件中
const props = defineProps(['userId'])
const { user } = useUserDetail(() => props.userId) // 传 getter 也行
[⬆ 返回目录](#⬆ 返回目录)
七、常见坑与避坑指南
7.1 忘记处理响应式
javascript
// ❌ 错误:直接解构会丢失响应式
function useCounter() {
return { count: ref(0), increment: () => {} }
}
const { count } = useCounter()
// count 在模板中不会更新!
javascript
// ✅ 正确:返回 ref,使用时保持 ref
const { count, increment } = useCounter()
// 模板中直接用 count,或用 count.value 访问
在 <script setup> 顶层解构 ref 是没问题的,模板会自动解包;在普通函数里再解构就要注意保持 ref 引用。
[⬆ 返回目录](#⬆ 返回目录)
7.2 生命周期和副作用没清理
javascript
// ❌ 错误:定时器、事件监听没有清理
function usePolling(callback, interval) {
setInterval(callback, interval) // 组件销毁后仍在执行
return {}
}
javascript
// ✅ 正确:在 onUnmounted 中清理
function usePolling(callback, interval) {
const timer = setInterval(callback, interval)
onUnmounted(() => clearInterval(timer))
return {}
}
[⬆ 返回目录](#⬆ 返回目录)
7.3 把组合式函数当工具函数用
javascript
// ❌ 错误:在非 setup 中调用,没有响应式上下文
async function handleClick() {
const { data } = useRequest('/api/xxx') // 每次点击都新建,不会累积
await ...
}
组合式函数必须在 setup 或 <script setup> 的顶层同步调用,不能放在事件回调、异步函数里。
[⬆ 返回目录](#⬆ 返回目录)
八、小结:一套可直接照搬的检查清单
封装组合式函数时,可以按下面 checklist 自检:
- 命名
- 以
use开头 - 能看出功能和用途
- 输入
- 参数尽量用对象 + 默认值
- 不把业务细节写死在 Hook 里
- 输出
- 只返回外部会用到的内容
- 不暴露内部实现细节
- 复用边界
- 满足 3 次再用再抽
- 区分:纯逻辑 vs 业务 vs 全局状态
- 副作用
- 定时器、监听等在
onUnmounted中清理
照着这些规范来,组合式函数的可读性和复用性都会好很多。
[⬆ 返回目录](#⬆ 返回目录)
🔍 系列模块导航
📝 Vue 组件与模板规范
一、《Vue3 组件拆分实战规范:页面 / 业务 / 基础组件边界清晰化,高内聚低耦合落地指南|Vue 组件与模板规范篇》
二、《Vue3 Props 传参实战规范:必传校验 + 默认值 + 类型标注,避开 undefined / 类型混用坑|Vue 组件与模板规范篇》
三、《Vue3 模板语法规范实战:v-if/v-for 不混用 + 表达式精简,避坑指南|Vue 组件与模板规范篇》
四、《Vue3 样式实战:scoped + 深度选择器 + BEM 规范,解决冲突与穿透失效|Vue 组件与模板规范篇》
五、《Vue3 组合式函数(Hooks)封装规范实战:命名 / 输入输出 / 复用边界 + 避坑|Vue 组件与模板规范篇》
六、《Vue3 + Element Plus 中后台弹窗规范:开闭、传参、回调,告别弹窗地狱|Vue 组件与模板规范篇》
七、《Vue3 组件解耦实战:Props/Emit/ 事件总线用法 + 避坑指南|Vue 组件与模板规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
「前端规范实战系列 」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。
更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~