組合式函數
TIP
此章節假設你已經對組合式 API 有了基本的了解。如果你只學習過選項式 API,你可以使用左側邊欄上方的切換按鈕將 API 風格切換為組合式 API 後,重新閱讀 響應性基礎 和 生命週期鉤子 兩個章節。
什麼是“組合式函數”?
在 Vue 應用的概念中,“組合式函數”(Composables) 是一個利用 Vue 的組合式 API 來封裝和複用 有狀態邏輯 的函數。
當構建前端應用時,我們常常需要複用公共任務的邏輯。例如為了在不同地方格式化時間,我們可能會抽取一個可複用的日期格式化函數。這個函數封裝了 無狀態的邏輯 :它在接收一些輸入後立刻返回所期望的輸出。複用無狀態邏輯的庫有很多,例如你可能已經用過的 lodash 或是 date-fns 。
相比之下,有狀態邏輯負責管理會隨時間而變化的狀態。一個簡單的例子是跟蹤當前鼠標在頁面中的位置。在實際應用中,也可能是像觸摸手勢或與數據庫的連接狀態這樣的更復雜的邏輯。
鼠標跟蹤器示例
如果我們要直接在組件中使用組合式 API 實現鼠標跟蹤功能,它會是這樣的:
但是,如果我們想在多個組件中複用這個相同的邏輯呢?我們可以把這個邏輯以一個組合式函數的形式提取到外部文件中:
下面是它在組件中使用的方式:
如你所見,核心邏輯完全一致,我們做的只是把它移到一個外部函數中去,並返回需要暴露的狀態。和在組件中一樣,你也可以在組合式函數中使用所有的
組合式 API
。現在,
useMouse()
的功能可以在任何組件中輕易複用了。
更酷的是,你還可以嵌套多個組合式函數:一個組合式函數可以調用一個或多個其他的組合式函數。這使得我們可以像使用多個組件組合成整個應用一樣,用多個較小且邏輯獨立的單元來組合形成複雜的邏輯。實際上,這正是為什麼我們決定將實現了這一設計模式的 API 集合命名為組合式 API。
舉例來說,我們可以將添加和清除 DOM 事件監聽器的邏輯也封裝進一個組合式函數中:
有了它,之前的
useMouse()
組合式函數可以被簡化為:
TIP
每一個調用
useMouse()
的組件實例會創建其獨有的
x
、
y
狀態拷貝,因此他們不會互相影響。如果你想要在組件之間共享狀態,請閱讀
狀態管理
這一章。
異步狀態示例
useMouse()
組合式函數沒有接收任何參數,因此讓我們再來看一個需要接收一個參數的組合式函數示例。在做異步數據請求時,我們常常需要處理不同的狀態:加載中、加載成功和加載失敗。
如果在每個需要獲取數據的組件中都要重複這種模式,那就太繁瑣了。我們可以把它抽離成一個組合式函數:
現在我們在組件裡只需要:
接收響應式狀態
useFetch()
接收一個靜態 URL 字符串作為輸入——因此它只會執行一次 fetch 並且就此結束。如果我們想要在 URL 改變時重新 fetch 呢?為了實現這一點,我們需要將響應式狀態傳入組合式函數,並讓它基於傳入的狀態來創建執行操作的偵聽器。
舉例來說,
useFetch()
應該能夠接收一個 ref:
或者接收一個 getter 函數 :
我們可以用
watchEffect()
和
toValue()
API 來重構我們現有的實現:
toValue()
是一個在 3.3 版本中新增的 API。它的設計目的是將 ref 或 getter 規範化為值。如果參數是 ref,它會返回 ref 的值;如果參數是函數,它會調用函數並返回其返回值。否則,它會原樣返回參數。它的工作方式類似於
unref()
,但對函數有特殊處理。
注意
toValue(url)
是在
watchEffect
回調函數的
內部
調用的。這確保了在
toValue()
規範化期間訪問的任何響應式依賴項都會被偵聽器跟蹤。
這個版本的
useFetch()
現在能接收靜態 URL 字符串、ref 和 getter,使其更加靈活。watch effect 會立即運行,並且會跟蹤
toValue(url)
期間訪問的任何依賴項。如果沒有跟蹤到依賴項 (例如 url 已經是字符串),則 effect 只會運行一次;否則,它將在跟蹤到的任何依賴項更改時重新運行。
這是
更新後的
useFetch()
,為了便於演示,添加了人為延遲和隨機錯誤。
約定和最佳實踐
命名
組合式函數約定用駝峰命名法命名,並以“use”作為開頭。
輸入參數
即便不依賴於 ref 或 getter 的響應性,組合式函數也可以接收它們作為參數。如果你正在編寫一個可能被其他開發者使用的組合式函數,最好處理一下輸入參數是 ref 或 getter 而非原始值的情況。可以利用
toValue()
工具函數來實現:
如果你的組合式函數在輸入參數是 ref 或 getter 的情況下創建了響應式 effect,為了讓它能夠被正確追蹤,請確保要麼使用
watch()
顯式地監視 ref 或 getter,要麼在
watchEffect()
中調用
toValue()
。
前面討論過的 useFetch() 實現 提供了一個接受 ref、getter 或普通值作為輸入參數的組合式函數的具體示例。
返回值
你可能已經注意到了,我們一直在組合式函數中使用
ref()
而不是
reactive()
。我們推薦的約定是組合式函數始終返回一個包含多個 ref 的普通的非響應式對象,這樣該對象在組件中被解構為 ref 之後仍可以保持響應性:
從組合式函數返回一個響應式對象會導致在對象解構過程中丟失與組合式函數內狀態的響應性連接。與之相反,ref 則可以維持這一響應性連接。
如果你更希望以對象屬性的形式來使用組合式函數中返回的狀態,你可以將返回的對象用
reactive()
包裝一次,這樣其中的 ref 會被自動解包,例如:
副作用
在組合式函數中的確可以執行副作用 (例如:添加 DOM 事件監聽器或者請求數據),但請注意以下規則:
-
如果你的應用用到了 服務端渲染 (SSR),請確保在組件掛載後才調用的生命週期鉤子中執行 DOM 相關的副作用,例如:
onMounted()
。這些鉤子只會在瀏覽器中被調用,因此可以確保能訪問到 DOM。 -
確保在
onUnmounted()
時清理副作用。舉例來說,如果一個組合式函數設置了一個事件監聽器,它就應該在onUnmounted()
中被移除 (就像我們在useMouse()
示例中看到的一樣)。當然也可以像之前的useEventListener()
示例那樣,使用一個組合式函數來自動幫你做這些事。
使用限制
組合式函數只能在
<script setup>
或
setup()
鉤子中被調用。在這些上下文中,它們也只能被
同步
調用。在某些情況下,你也可以在像
onMounted()
這樣的生命週期鉤子中調用它們。
這些限制很重要,因為這些是 Vue 用於確定當前活躍的組件實例的上下文。訪問活躍的組件實例很有必要,這樣才能:
-
將生命週期鉤子註冊到該組件實例上
-
將計算屬性和監聽器註冊到該組件實例上,以便在該組件被卸載時停止監聽,避免內存洩漏。
TIP
<script setup>
是唯一在調用
await
之後
仍可調用組合式函數的地方。編譯器會在異步操作之後自動為你恢復當前的組件實例。
通過抽取組合式函數改善代碼結構
抽取組合式函數不僅是為了複用,也是為了代碼組織。隨著組件複雜度的增高,你可能會最終發現組件多得難以查詢和理解。組合式 API 會給予你足夠的靈活性,讓你可以基於邏輯問題將組件代碼拆分成更小的函數:
在某種程度上,你可以將這些提取出的組合式函數看作是可以相互通信的組件範圍內的服務。
在選項式 API 中使用組合式函數
如果你正在使用選項式 API,組合式函數必須在
setup()
中調用。且其返回的綁定必須在
setup()
中返回,以便暴露給
this
及其模板: