此章節(jié)假設(shè)你已經(jīng)對(duì)組合式 API 有了基本的了解。如果你只學(xué)習(xí)過選項(xiàng)式 API,你可以使用左側(cè)邊欄上方的切換按鈕將 API 風(fēng)格切換為組合式 API 后,重新閱讀響應(yīng)性基礎(chǔ)和生命周期鉤子兩個(gè)章節(jié)。
:::
在 Vue 應(yīng)用的概念中,“組合式函數(shù)”(Composables) 是一個(gè)利用 Vue 的組合式 API 來封裝和復(fù)用有狀態(tài)邏輯的函數(shù)。
當(dāng)構(gòu)建前端應(yīng)用時(shí),我們常常需要復(fù)用公共任務(wù)的邏輯。例如為了在不同地方格式化時(shí)間,我們可能會(huì)抽取一個(gè)可復(fù)用的日期格式化函數(shù)。這個(gè)函數(shù)封裝了無狀態(tài)的邏輯:它在接收一些輸入后立刻返回所期望的輸出。復(fù)用無狀態(tài)邏輯的庫有很多,比如你可能已經(jīng)用過的 lodash 或是 date-fns。
相比之下,有狀態(tài)邏輯負(fù)責(zé)管理會(huì)隨時(shí)間而變化的狀態(tài)。一個(gè)簡單的例子是跟蹤當(dāng)前鼠標(biāo)在頁面中的位置。在實(shí)際應(yīng)用中,也可能是像觸摸手勢(shì)或與數(shù)據(jù)庫的連接狀態(tài)這樣的更復(fù)雜的邏輯。
如果我們要直接在組件中使用組合式 API 實(shí)現(xiàn)鼠標(biāo)跟蹤功能,它會(huì)是這樣的:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
但是,如果我們想在多個(gè)組件中復(fù)用這個(gè)相同的邏輯呢?我們可以把這個(gè)邏輯以一個(gè)組合式函數(shù)的形式提取到外部文件中:
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照慣例,組合式函數(shù)名以“use”開頭
export function useMouse() {
// 被組合式函數(shù)封裝和管理的狀態(tài)
const x = ref(0)
const y = ref(0)
// 組合式函數(shù)可以隨時(shí)更改其狀態(tài)。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一個(gè)組合式函數(shù)也可以掛靠在所屬組件的生命周期上
// 來啟動(dòng)和卸載副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通過返回值暴露所管理的狀態(tài)
return { x, y }
}
下面是它在組件中使用的方式:
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
<div class="demo"> Mouse position is at: {{ x }}, {{ y }} </div>
如你所見,核心邏輯完全一致,我們做的只是把它移到一個(gè)外部函數(shù)中去,并返回需要暴露的狀態(tài)。和在組件中一樣,你也可以在組合式函數(shù)中使用所有的組合式 API?,F(xiàn)在,useMouse()
的功能可以在任何組件中輕易復(fù)用了。
更酷的是,你還可以嵌套多個(gè)組合式函數(shù):一個(gè)組合式函數(shù)可以調(diào)用一個(gè)或多個(gè)其他的組合式函數(shù)。這使得我們可以像使用多個(gè)組件組合成整個(gè)應(yīng)用一樣,用多個(gè)較小且邏輯獨(dú)立的單元來組合形成復(fù)雜的邏輯。實(shí)際上,這正是為什么我們決定將實(shí)現(xiàn)了這一設(shè)計(jì)模式的 API 集合命名為組合式 API。
舉例來說,我們可以將添加和清除 DOM 事件監(jiān)聽器的邏輯也封裝進(jìn)一個(gè)組合式函數(shù)中:
// event.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// 如果你想的話,
// 也可以用字符串形式的 CSS 選擇器來尋找目標(biāo) DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
有了它,之前的 useMouse()
組合式函數(shù)可以被簡化為:
```js{3,9-12} // mouse.js import { ref } from 'vue' import { useEventListener } from './event'
export function useMouse() { const x = ref(0) const y = ref(0)
useEventListener(window, 'mousemove', (event) => { x.value = event.pageX y.value = event.pageY })
return { x, y } }
:::tip
每一個(gè)調(diào)用 `useMouse()` 的組件實(shí)例會(huì)創(chuàng)建其獨(dú)有的 `x`、`y` 狀態(tài)拷貝,因此他們不會(huì)互相影響。如果你想要在組件之間共享狀態(tài),請(qǐng)閱讀[狀態(tài)管理](/guide/scaling-up/state-management)這一章。
:::
## 異步狀態(tài)示例 {#async-state-example}
`useMouse()` 組合式函數(shù)沒有接收任何參數(shù),因此讓我們?cè)賮砜匆粋€(gè)需要接收一個(gè)參數(shù)的組合式函數(shù)示例。在做異步數(shù)據(jù)請(qǐng)求時(shí),我們常常需要處理不同的狀態(tài):加載中、加載成功和加載失敗。
<script setup> import { ref } from 'vue'
const data = ref(null) const error = ref(null)
fetch('...') .then((res) => res.json()) .then((json) => (data.value = json)) .catch((err) => (error.value = err)) </script>
<template> <div v-if="error">Oops! Error encountered: {{ error.message }}</div> <div v-else-if="data"> Data loaded: <pre>{{ data }}</pre> </div> <div v-else>Loading...</div> </template>
如果在每個(gè)需要獲取數(shù)據(jù)的組件中都要重復(fù)這種模式,那就太繁瑣了。讓我們把它抽取成一個(gè)組合式函數(shù):
```js
// fetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}
現(xiàn)在我們?cè)诮M件里只需要:
<script setup>
import { useFetch } from './fetch.js'
const { data, error } = useFetch('...')
</script>
useFetch()
接收一個(gè)靜態(tài) URL 字符串作為輸入——因此它只會(huì)執(zhí)行一次 fetch 并且就此結(jié)束。如果我們想要在 URL 改變時(shí)重新 fetch 呢?為了實(shí)現(xiàn)這一點(diǎn),我們需要將響應(yīng)式狀態(tài)傳入組合式函數(shù),并讓它基于傳入的狀態(tài)來創(chuàng)建執(zhí)行操作的偵聽器。
舉例來說,useFetch()
應(yīng)該能夠接收一個(gè) ref:
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 這將會(huì)重新觸發(fā) fetch
url.value = '/new-url'
或者接收一個(gè) getter 函數(shù):
// 當(dāng) props.id 改變時(shí)重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)
我們可以用 watchEffect()
和 toValue()
API 來重構(gòu)我們現(xiàn)有的實(shí)現(xiàn):
```js{8,13} // fetch.js import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) { const data = ref(null) const error = ref(null)
watchEffect(() => { // 在 fetch 之前重置狀態(tài) data.value = null error.value = null // toValue() 將可能的 ref 或 getter 解包 fetch(toValue(url)) .then((res) => res.json()) .then((json) => (data.value = json)) .catch((err) => (error.value = err)) })
return { data, error } }
`toValue()` 是一個(gè)在 3.3 版本中新增的 API。它的設(shè)計(jì)目的是將 ref 或 getter 規(guī)范化為值。如果參數(shù)是 ref,它會(huì)返回 ref 的值;如果參數(shù)是函數(shù),它會(huì)調(diào)用函數(shù)并返回其返回值。否則,它會(huì)原樣返回參數(shù)。它的工作方式類似于 [`unref()`](/api/reactivity-utilities.html#unref),但對(duì)函數(shù)有特殊處理。
注意 `toValue(url)` 是在 `watchEffect` 回調(diào)函數(shù)的**內(nèi)部**調(diào)用的。這確保了在 `toValue()` 規(guī)范化期間訪問的任何響應(yīng)式依賴項(xiàng)都會(huì)被偵聽器跟蹤。
這個(gè)版本的 `useFetch()` 現(xiàn)在能接收靜態(tài) URL 字符串、ref 和 getter,使其更加靈活。watch effect 會(huì)立即運(yùn)行,并且會(huì)跟蹤 `toValue(url)` 期間訪問的任何依賴項(xiàng)。如果沒有跟蹤到依賴項(xiàng)(例如 url 已經(jīng)是字符串),則 effect 只會(huì)運(yùn)行一次;否則,它將在跟蹤到的任何依賴項(xiàng)更改時(shí)重新運(yùn)行。
這是[更新后的 `useFetch()`](https://play.vuejs.org/#eNptVMFu2zAM/RXOFztYZncodgmSYAPWnTZsKLadfFFsulHrSIZEJwuC/PtIyXaTtkALxxT5yPf45FPypevyfY/JIln6yumOwCP13bo0etdZR3ACh80cKrvresIaztA4u4OUi9KLpN7jN6RqO53nxRjKHz1nlqayxhNslMc/roUVpFuizi+K4tFb07Wqwq1ta3Q5HTtd2RpzblqQra0vGCCW65oreaIs/ZjOxmAf8MYRs2wGq/XU6D3X5HvV9sj5Y8UJakVqDuicdXMGJHfk0VcTj4wxOX9ZRFVYD34h3PGchPwG8N2qGjobZlpIYLnpiayB/YfGulWZaNAGPpUJfK5aXT1JRIbXZbI+nUDD+bwsYklAL2lZ6z1X64ZTw2CcKcAM3a1/2s6/gzsJAzKL3hA6rBfAWCE536H36gEDriwwFA4zTSMEpox7L8+L/pxacPv4K86Brcc4jGjFNV/5AS3TlrbLzqHwkLPYkt/fxFiLUto85Hk+ni+LScpknlwYhX147buD4oO7psGK5kD2r+zxhQdLg/9CSdObijSzvVoinGSeuPYwbPSP6VtZ8HgSJHx5JP8XA2TKH00F0V4BFaAouISvDHhiNrBB3j1CI90D5ZglfaMHuYXAx3Dc2+v4JbRt9wi0xWDymCpTbJ01tvftEbwFTakHcqp64guqPKgJoMYOTc1+OcLmeMUlEBzZM3ZUdjVqPPj/eRq5IAPngKwc6UZXWrXcpFVH4GmVqXkt0boiHwGog9IEpHdo+6GphBmgN6L1DA66beUC9s4EnhwdeOomMlMSkwsytLac5g7aR11ibkDZSLUABRk+aD8QoMiS1WSCcaKwISEZ2MqXIaBfLSpmchUb05pRsTNUIiNkOFjr9SZxyJTHOXx1YGR49eGRDP4rzRt6lmay86Re7DcgGTzAL74GrEOWDUaRL9kjb/fSoWzO3wPAlXNB9M1+KNrmcXF8uoab/PaCljQLwCN5oS93+jpFWmYyT/g8Zel9NEJ4S2fPpYMsc7i9uQlREeecnP8DWEwr0Q==),為了便于演示,添加了人為延遲和隨機(jī)錯(cuò)誤。
## 約定和最佳實(shí)踐 {#conventions-and-best-practices}
### 命名 {#naming}
組合式函數(shù)約定用駝峰命名法命名,并以“use”作為開頭。
### 輸入?yún)?shù) {#input-arguments}
即便不依賴于 ref 或 getter 的響應(yīng)性,組合式函數(shù)也可以接收它們作為參數(shù)。如果你正在編寫一個(gè)可能被其他開發(fā)者使用的組合式函數(shù),最好處理一下輸入?yún)?shù)是 ref 或 getter 而非原始值的情況??梢岳?[`toValue()`](/api/reactivity-utilities#tovalue) 工具函數(shù)來實(shí)現(xiàn):
```js
import { toValue } from 'vue'
function useFeature(maybeRefOrGetter) {
// 如果 maybeRefOrGetter 是一個(gè) ref 或 getter,
// 將返回它的規(guī)范化值。
// 否則原樣返回。
const value = toValue(maybeRefOrGetter)
}
如果你的組合式函數(shù)在輸入?yún)?shù)是 ref 或 getter 的情況下創(chuàng)建了響應(yīng)式 effect,為了讓它能夠被正確追蹤,請(qǐng)確保要么使用 watch()
顯式地監(jiān)視 ref 或 getter,要么在 watchEffect()
中調(diào)用 toValue()
。
前面討論過的 useFetch() 實(shí)現(xiàn)提供了一個(gè)接受 ref、getter 或普通值作為輸入?yún)?shù)的組合式函數(shù)的具體示例。
你可能已經(jīng)注意到了,我們一直在組合式函數(shù)中使用 ref()
而不是 reactive()
。我們推薦的約定是組合式函數(shù)始終返回一個(gè)包含多個(gè) ref 的普通的非響應(yīng)式對(duì)象,這樣該對(duì)象在組件中被解構(gòu)為 ref 之后仍可以保持響應(yīng)性:
// x 和 y 是兩個(gè) ref
const { x, y } = useMouse()
從組合式函數(shù)返回一個(gè)響應(yīng)式對(duì)象會(huì)導(dǎo)致在對(duì)象解構(gòu)過程中丟失與組合式函數(shù)內(nèi)狀態(tài)的響應(yīng)性連接。與之相反,ref 則可以維持這一響應(yīng)性連接。
如果你更希望以對(duì)象屬性的形式來使用組合式函數(shù)中返回的狀態(tài),你可以將返回的對(duì)象用 reactive()
包裝一次,這樣其中的 ref 會(huì)被自動(dòng)解包,例如:
const mouse = reactive(useMouse())
// mouse.x 鏈接到了原來的 x ref
console.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
在組合式函數(shù)中的確可以執(zhí)行副作用 (例如:添加 DOM 事件監(jiān)聽器或者請(qǐng)求數(shù)據(jù)),但請(qǐng)注意以下規(guī)則:
onMounted()
。這些鉤子僅會(huì)在瀏覽器中被調(diào)用,因此可以確保能訪問到 DOM。onUnmounted()
時(shí)清理副作用。舉例來說,如果一個(gè)組合式函數(shù)設(shè)置了一個(gè)事件監(jiān)聽器,它就應(yīng)該在 onUnmounted()
中被移除 (就像我們?cè)?useMouse()
示例中看到的一樣)。當(dāng)然也可以像之前的 useEventListener()
示例那樣,使用一個(gè)組合式函數(shù)來自動(dòng)幫你做這些事。
組合式函數(shù)只能在 <script setup>
或 setup()
鉤子中被調(diào)用。在這些上下文中,它們也只能被同步調(diào)用。在某些情況下,你也可以在像 onMounted()
這樣的生命周期鉤子中調(diào)用它們。
這些限制很重要,因?yàn)檫@些是 Vue 用于確定當(dāng)前活躍的組件實(shí)例的上下文。訪問活躍的組件實(shí)例很有必要,這樣才能:
:::tip
<script setup>
是唯一在調(diào)用 await
之后仍可調(diào)用組合式函數(shù)的地方。編譯器會(huì)在異步操作之后自動(dòng)為你恢復(fù)當(dāng)前的組件實(shí)例。
:::
抽取組合式函數(shù)不僅是為了復(fù)用,也是為了代碼組織。隨著組件復(fù)雜度的增高,你可能會(huì)最終發(fā)現(xiàn)組件多得難以查詢和理解。組合式 API 會(huì)給予你足夠的靈活性,讓你可以基于邏輯問題將組件代碼拆分成更小的函數(shù):
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'
const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>
在某種程度上,你可以將這些提取出的組合式函數(shù)看作是可以相互通信的組件范圍內(nèi)的服務(wù)。
如果你正在使用選項(xiàng)式 API,組合式函數(shù)必須在 setup()
中調(diào)用。且其返回的綁定必須在 setup()
中返回,以便暴露給 this
及其模板:
import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'
export default {
setup() {
const { x, y } = useMouse()
const { data, error } = useFetch('...')
return { x, y, data, error }
},
mounted() {
// setup() 暴露的屬性可以在通過 `this` 訪問到
console.log(this.x)
}
// ...其他選項(xiàng)
}
Vue 2 的用戶可能會(huì)對(duì) mixins 選項(xiàng)比較熟悉。它也讓我們能夠把組件邏輯提取到可復(fù)用的單元里。然而 mixins 有三個(gè)主要的短板:
基于上述理由,我們不再推薦在 Vue 3 中繼續(xù)使用 mixin。保留該功能只是為了項(xiàng)目遷移的需求和照顧熟悉它的用戶。
在組件插槽一章中,我們討論過了基于作用域插槽的無渲染組件。我們甚至用它實(shí)現(xiàn)了一樣的鼠標(biāo)追蹤器示例。
組合式函數(shù)相對(duì)于無渲染組件的主要優(yōu)勢(shì)是:組合式函數(shù)不會(huì)產(chǎn)生額外的組件實(shí)例開銷。當(dāng)在整個(gè)應(yīng)用中使用時(shí),由無渲染組件產(chǎn)生的額外組件實(shí)例會(huì)帶來無法忽視的性能開銷。
我們推薦在純邏輯復(fù)用時(shí)使用組合式函數(shù),在需要同時(shí)復(fù)用邏輯和視圖布局時(shí)使用無渲染組件。
如果你有 React 的開發(fā)經(jīng)驗(yàn),你可能注意到組合式函數(shù)和自定義 React hooks 非常相似。組合式 API 的一部分靈感正來自于 React hooks,Vue 的組合式函數(shù)也的確在邏輯組合能力上與 React hooks 相近。然而,Vue 的組合式函數(shù)是基于 Vue 細(xì)粒度的響應(yīng)性系統(tǒng),這和 React hooks 的執(zhí)行模型有本質(zhì)上的不同。這一話題在組合式 API 的常見問題中有更細(xì)致的討論。
更多建議: