Vue Composables - 组合式函数的应用

Vue3 带来最大的革新莫过于选项式API (Composition API), 其中一大特性就是组合式函数。在 Vue 生态中,composables 一般翻译成 “组合式函数” 或 “可组合函数”, 意思是把一段逻辑(通常是响应式状态、计算属性、侦听器等)封装成一个独立的函数,这个函数可以在多个组件中复用, 也可以用于解耦臃肿的业务逻辑。

作用:逻辑复用、代码组织更清晰,不依赖组件上下文

一个简单的组合式函数样例

创建 useGrid.ts 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { computed, ref } from "vue"

type Grid = {
columnId: number,
rowId: number,
resolution: number
}

export const useGrid = () => {
const columnId = ref<number>()
const rowId = ref<number>()
const resolution = ref<number>()
const selectedGrid = ref<Grid>()

const getAllGrid = () => {
// 在这里封装具体的函数逻辑
...
}

return {
selectedGrid,
getAllGrid
}
}

在你的代码中应用

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
一个简单的组合式函数样例🚀
<span>当前选中的格网:{{ selectedGrid }}</span>
</div>
</template>

<script setup lang="ts">
import { useGrid } from "./useGrid.ts"
const { selectedGrid, getAllGrid } = useGrid() // 记住这种写法!对象解耦
</script>

等同于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>
一个简单的非组合式函数样例
<span>当前选中的格网:{{ selectedGrid }}</span>
</div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue"

type Grid = {
columnId: number,
rowId: number,
resolution: number
}

const columnId = ref<number>()
const rowId = ref<number>()
const resolution = ref<number>()
const selectedGrid = ref<Grid>()

const getAllGrid = () => {
// 在这里封装具体的函数逻辑
...
}
</script>

组合式函数的好处

大家可以发现,组合式函数能够将你的业务逻辑从原本臃肿的代码中抽象解放出来!以便于逻辑复用,同时代码组织更加清晰,不依赖组件上下文

组合式函数的“陷阱”

选项式 API 作用域范围混淆——案例1

大家可以思考一下以下两种写法有什么不同?

  • 写法1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// useGrid.ts
import { computed, ref } from "vue"

export const useGrid = () => {
// 把响应式变量封装入组合式函数内部
const columnId = ref<number>(0)

return {
columnId
}
}

// test.ts
import { useGrid } from "./useGrid.ts"
const { columnId: columnIdA } = useGrid() // :相当于以columnIdA的变量名引入columnId
const { columnId: columnIdB } = useGrid()

columnIdA.value ++
columnIdB.value ++

console.log(columnIdA.value)
console.log(columnIdB.value)

输出结果

1
2
1
1
  • 写法2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// useGrid.ts
import { computed, ref } from "vue"

const columnId = ref<number>(0)

export const useGrid = () => {
return {
columId
}
}

// test.ts
import { useGrid } from "./useGrid.ts"
const { columnId: columnIdA } = useGrid() // :相当于以columnIdA的变量名引入columnId
const { columnId: columnIdB } = useGrid()

columnIdA.value ++
columnIdB.value ++

console.log(columnIdA.value)
console.log(columnIdB.value)

输出结果

1
2
1
2

为什么会造成这种差异呢?

因为写法1把columnId这个响应式变量作为了组合函数封装的一部分,而写法2则把columnId作为了全局变量。在写法1的情况下,利用useGrid声明的两个响应式变量是双胞胎兄弟,它们本质上是不同的个体!而写法2虽然也声明了两个响应式变量,但本质上两个响应式变量指向的是同一个变量。

这两种做法各有什么实际用处呢?

写法1更适用于“逻辑复用”,在不同的地方声明useGrid,也即在不同的业务场景应用组合式函数,他们需要的是独立的响应式变量,不需要共享给其他组件。
而写法2更适用于“共享”,在不同的地方声明useGrid,它们可以拿到同一份响应式变量,需要的是能在不同的业务场景中共享一份数据。

选项式API作用域范围混淆——案例2

前情提要:useI18n是一个只能作用于选项式API范围内的组合式函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// useA.ts
export const useA = () => {
const { t } = useI18n()
...

return {
...
}
}

//useB.ts
import { useA } from './useA.ts'
const { ... } = useA()

export const useB = () => {
...
return {
...
}
}

vue组件测试

1
2
3
4
5
6
7
8
<template>
<div></div>
</template>

<script setup lang="ts">
import { useB } from './useB.ts'
const { ... } = useB()
</script>

这是另一种作用域混淆错误,因为useA中useI18n只能在选项式API内声明,而useB中却在组合式函数外部声明useA,这本质上脱离了选项式API的作用域,故vue组件就会报错useI18n必须在选项式API内部声明。

注意规避循环依赖

有的时候我们喜欢混用各个组合式函数,组合式函数之间可能存在相互引用的变量,就会造成循环依赖,故在设计时就要将各个模块解耦开,另外推荐一种笔者推荐的写法,可以有效避免循环依赖:

笔者推荐的写法

1
2
3
4
5
6
7
8
9
10
// 创建 shared.ts
import { ref } from 'vue'
export const commonVar = ref<number>(0)

// useA.ts
import { commonVar } from './shared.ts'
... // 使用commonVar
// useB.ts
import { commonVar } from './shared.ts'
... // 使用commonVar

Vue Composables - 组合式函数的应用
http://example.com/2025/08/09/Vue-Composables-组合式函数的应用/
作者
Lingkai Shi
发布于
2025年8月9日
许可协议