此章(zhāng)節假設你已經對組合式 API 有了(le)基本的了(le)解。如(rú)果你隻學習過選項式 API,你可(kě)以使用左側邊欄上(shàng)方的切換按鈕将 API 風(fēng)格切換為(wèi)組合式 API 後,重新閱讀響應性基礎和(hé)生命周期鈎子兩個章(zhāng)節。
什麽是“組合式函數(shù)”?
在 Vue 應用的概念中,“組合式函數(shù)”(Composables) 是一(yī)個利用 Vue 的組合式 API 來封裝和(hé)複用有狀态邏輯的函數(shù)。
當構建前端應用時(shí),我們常常需要(yào)複用公共任務的邏輯。例如(rú)為(wèi)了(le)在不同地(dì)方格式化時(shí)間(jiān),我們可(kě)能(néng)會抽取一(yī)個可(kě)複用的日期格式化函數(shù)。這(zhè)個函數(shù)封裝了(le)無狀态的邏輯:它在接收一(yī)些輸入後立刻返回所期望的輸出。複用無狀态邏輯的庫有很(hěn)多,比如(rú)你可(kě)能(néng)已經用過的 lodash 或是 date-fns。
相比之下(xià),有狀态邏輯負責管理會随時(shí)間(jiān)而變化的狀态。一(yī)個簡單的例子是跟蹤當前鼠标在頁面中的位置。在實際應用中,也可(kě)能(néng)是像觸摸手勢或與數(shù)據庫的連接狀态這(zhè)樣的更複雜(zá)的邏輯。
鼠标跟蹤器示例
如(rú)果我們要(yào)直接在組件中使用組合式 API 實現鼠标跟蹤功能(néng),它會是這(zhè)樣的:
vue
<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>
但(dàn)是,如(rú)果我們想在多個組件中複用這(zhè)個相同的邏輯呢?我們可(kě)以把這(zhè)個邏輯以一(yī)個組合式函數(shù)的形式提取到外(wài)部文件中:
js
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照慣例,組合式函數(shù)名以“use”開(kāi)頭
export function useMouse() {
// 被組合式函數(shù)封裝和(hé)管理的狀态
const x = ref(0)
const y = ref(0)
// 組合式函數(shù)可(kě)以随時(shí)更改其狀态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一(yī)個組合式函數(shù)也可(kě)以挂靠在所屬組件的生命周期上(shàng)
// 來啓動和(hé)卸載副作(zuò)用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通(tōng)過返回值暴露所管理的狀态
return { x, y }
}
下(xià)面是它在組件中使用的方式:
vue
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
Mouse position is at: 1682, 2666
如(rú)你所見(jiàn),核心邏輯完全一(yī)緻,我們做(zuò)的隻是把它移到一(yī)個外(wài)部函數(shù)中去,并返回需要(yào)暴露的狀态。和(hé)在組件中一(yī)樣,你也可(kě)以在組合式函數(shù)中使用所有的組合式 API。現在,useMouse() 的功能(néng)可(kě)以在任何組件中輕易複用了(le)。
更酷的是,你還可(kě)以嵌套多個組合式函數(shù):一(yī)個組合式函數(shù)可(kě)以調用一(yī)個或多個其他(tā)的組合式函數(shù)。這(zhè)使得我們可(kě)以像使用多個組件組合成整個應用一(yī)樣,用多個較小且邏輯獨立的單元來組合形成複雜(zá)的邏輯。實際上(shàng),這(zhè)正是為(wèi)什麽我們決定将實現了(le)這(zhè)一(yī)設計模式的 API 集合命名為(wèi)組合式 API。
舉例來說,我們可(kě)以将添加和(hé)清除 DOM 事件監聽器的邏輯也封裝進一(yī)個組合式函數(shù)中:
js
// event.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// 如(rú)果你想的話,
// 也可(kě)以用字符串形式的 CSS 選擇器來尋找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
有了(le)它,之前的 useMouse() 組合式函數(shù)可(kě)以被簡化為(wèi):
js
// 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
每一(yī)個調用 useMouse() 的組件實例會創建其獨有的 x、y 狀态拷貝,因此他(tā)們不會互相影響。如(rú)果你想要(yào)在組件之間(jiān)共享狀态,請(qǐng)閱讀狀态管理這(zhè)一(yī)章(zhāng)。
異步狀态示例
useMouse() 組合式函數(shù)沒有接收任何參數(shù),因此讓我們再來看(kàn)一(yī)個需要(yào)接收一(yī)個參數(shù)的組合式函數(shù)示例。在做(zuò)異步數(shù)據請(qǐng)求時(shí),我們常常需要(yào)處理不同的狀态:加載中、加載成功和(hé)加載失敗。
vue
<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>
如(rú)果在每個需要(yào)獲取數(shù)據的組件中都(dōu)要(yào)重複這(zhè)種模式,那(nà)就太繁瑣了(le)。讓我們把它抽取成一(yī)個組合式函數(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 }
}
現在我們在組件裏隻需要(yào):
vue
<script setup>
import { useFetch } from './fetch.js'
const { data, error } = useFetch('...')
</script>
useFetch() 接收一(yī)個靜态的 URL 字符串作(zuò)為(wèi)輸入,所以它隻執行(xíng)一(yī)次請(qǐng)求,然後就完成了(le)。但(dàn)如(rú)果我們想讓它在每次 URL 變化時(shí)都(dōu)重新請(qǐng)求呢?那(nà)我們可(kě)以讓它同時(shí)允許接收 ref 作(zuò)為(wèi)參數(shù):
js
// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
function doFetch() {
// 在請(qǐng)求之前重設狀态...
data.value = null
error.value = null
// unref() 解包可(kě)能(néng)為(wèi) ref 的值
fetch(unref(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
if (isRef(url)) {
// 若輸入的 URL 是一(yī)個 ref,那(nà)麽啓動一(yī)個響應式的請(qǐng)求
watchEffect(doFetch)
} else {
// 否則隻請(qǐng)求一(yī)次
// 避免監聽器的額外(wài)開(kāi)銷
doFetch()
}
return { data, error }
}
這(zhè)個版本的 useFetch() 現在同時(shí)可(kě)以接收靜态的 URL 字符串和(hé) URL 字符串的 ref。當通(tōng)過 isRef() 檢測到 URL 是一(yī)個動态 ref 時(shí),它會使用 watchEffect() 啓動一(yī)個響應式的 effect。該 effect 會立刻執行(xíng)一(yī)次,并在此過程中将 URL 的 ref 作(zuò)為(wèi)依賴進行(xíng)跟蹤。當 URL 的 ref 發生改變時(shí),數(shù)據就會被重置,并重新請(qǐng)求。
這(zhè)裏是一(yī)個升級版的 useFetch(),出于演示目的,我們人(rén)為(wèi)地(dì)設置了(le)延遲和(hé)随機報錯。
約定和(hé)最佳實踐
命名
組合式函數(shù)約定用駝峰命名法命名,并以“use”作(zuò)為(wèi)開(kāi)頭。
輸入參數(shù)
盡管其響應性不依賴 ref,組合式函數(shù)仍可(kě)接收 ref 參數(shù)。如(rú)果編寫的組合式函數(shù)會被其他(tā)開(kāi)發者使用,你最好在處理輸入參數(shù)時(shí)兼容 ref 而不隻是原始的值。unref() 工(gōng)具函數(shù)會對此非常有幫助:
js
import { unref } from 'vue'
function useFeature(maybeRef) {
// 若 maybeRef 确實是一(yī)個 ref,它的 .value 會被返回
// 否則,maybeRef 會被原樣返回
const value = unref(maybeRef)
}
如(rú)果你的組合式函數(shù)在接收 ref 為(wèi)參數(shù)時(shí)會産生響應式 effect,請(qǐng)确保使用 watch() 顯式地(dì)監聽此 ref,或者在 watchEffect() 中調用 unref() 來進行(xíng)正确的追蹤。
返回值
你可(kě)能(néng)已經注意到了(le),我們一(yī)直在組合式函數(shù)中使用 ref() 而不是 reactive()。我們推薦的約定是組合式函數(shù)始終返回一(yī)個包含多個 ref 的普通(tōng)的非響應式對象,這(zhè)樣該對象在組件中被解構為(wèi) ref 之後仍可(kě)以保持響應性:
js
// x 和(hé) y 是兩個 ref
const { x, y } = useMouse()
從(cóng)組合式函數(shù)返回一(yī)個響應式對象會導緻在對象解構過程中丢失與組合式函數(shù)內(nèi)狀态的響應性連接。與之相反,ref 則可(kě)以維持這(zhè)一(yī)響應性連接。
如(rú)果你更希望以對象屬性的形式來使用組合式函數(shù)中返回的狀态,你可(kě)以将返回的對象用 reactive() 包裝一(yī)次,這(zhè)樣其中的 ref 會被自(zì)動解包,例如(rú):
js
const mouse = reactive(useMouse())
// mouse.x 鏈接到了(le)原來的 x ref
console.log(mouse.x)
template
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
副作(zuò)用
在組合式函數(shù)中的确可(kě)以執行(xíng)副作(zuò)用 (例如(rú):添加 DOM 事件監聽器或者請(qǐng)求數(shù)據),但(dàn)請(qǐng)注意以下(xià)規則:
如(rú)果你的應用用到了(le)服務端渲染 (SSR),請(qǐng)确保在組件挂載後才調用的生命周期鈎子中執行(xíng) DOM 相關的副作(zuò)用,例如(rú):onMounted()。這(zhè)些鈎子僅會在浏覽器中被調用,因此可(kě)以确保能(néng)訪問(wèn)到 DOM。
确保在 onUnmounted() 時(shí)清理副作(zuò)用。舉例來說,如(rú)果一(yī)個組合式函數(shù)設置了(le)一(yī)個事件監聽器,它就應該在 onUnmounted() 中被移除 (就像我們在 useMouse() 示例中看(kàn)到的一(yī)樣)。當然也可(kě)以像之前的 useEventListener() 示例那(nà)樣,使用一(yī)個組合式函數(shù)來自(zì)動幫你做(zuò)這(zhè)些事。
使用限制
組合式函數(shù)在 <script setup> 或 setup() 鈎子中,應始終被同步地(dì)調用。在某些場景下(xià),你也可(kě)以在像 onMounted() 這(zhè)樣的生命周期鈎子中使用他(tā)們。
這(zhè)個限制是為(wèi)了(le)讓 Vue 能(néng)夠确定當前正在被執行(xíng)的到底是哪個組件實例,隻有能(néng)确認當前組件實例,才能(néng)夠:
将生命周期鈎子注冊到該組件實例上(shàng)
将計算屬性和(hé)監聽器注冊到該組件實例上(shàng),以便在該組件被卸載時(shí)停止監聽,避免內(nèi)存洩漏。
TIP
<script setup> 是唯一(yī)在調用 await 之後仍可(kě)調用組合式函數(shù)的地(dì)方。編譯器會在異步操作(zuò)之後自(zì)動為(wèi)你恢複當前的組件實例。
通(tōng)過抽取組合式函數(shù)改善代碼結構
抽取組合式函數(shù)不僅是為(wèi)了(le)複用,也是為(wèi)了(le)代碼組織。随着組件複雜(zá)度的增高,你可(kě)能(néng)會最終發現組件多得難以查詢和(hé)理解。組合式 API 會給予你足夠的靈活性,讓你可(kě)以基于邏輯問(wèn)題将組件代碼拆分成更小的函數(shù):
vue
<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àng),你可(kě)以将這(zhè)些提取出的組合式函數(shù)看(kàn)作(zuò)是可(kě)以相互通(tōng)信的組件範圍內(nèi)的服務。
在選項式 API 中使用組合式函數(shù)
如(rú)果你正在使用選項式 API,組合式函數(shù)必須在 setup() 中調用。且其返回的綁定必須在 setup() 中返回,以便暴露給 this 及其模闆:
js
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() 暴露的屬性可(kě)以在通(tōng)過 `this` 訪問(wèn)到
console.log(this.x)
}
// ...其他(tā)選項
}
與其他(tā)模式的比較
和(hé) Mixin 的對比
Vue 2 的用戶可(kě)能(néng)會對 mixins 選項比較熟悉。它也讓我們能(néng)夠把組件邏輯提取到可(kě)複用的單元裏。然而 mixins 有三個主要(yào)的短闆:
不清晰的數(shù)據來源:當使用了(le)多個 mixin 時(shí),實例上(shàng)的數(shù)據屬性來自(zì)哪個 mixin 變得不清晰,這(zhè)使追溯實現和(hé)理解組件行(xíng)為(wèi)變得困難。這(zhè)也是我們推薦在組合式函數(shù)中使用 ref + 解構模式的理由:讓屬性的來源在消費(fèi)組件時(shí)一(yī)目了(le)然。
命名空間(jiān)沖突:多個來自(zì)不同作(zuò)者的 mixin 可(kě)能(néng)會注冊相同的屬性名,造成命名沖突。若使用組合式函數(shù),你可(kě)以通(tōng)過在解構變量時(shí)對變量進行(xíng)重命名來避免相同的鍵名。
隐式的跨 mixin 交流:多個 mixin 需要(yào)依賴共享的屬性名來進行(xíng)相互作(zuò)用,這(zhè)使得它們隐性地(dì)耦合在一(yī)起。而一(yī)個組合式函數(shù)的返回值可(kě)以作(zuò)為(wèi)另一(yī)個組合式函數(shù)的參數(shù)被傳入,像普通(tōng)函數(shù)那(nà)樣。
基于上(shàng)述理由,我們不再推薦在 Vue 3 中繼續使用 mixin。保留該功能(néng)隻是為(wèi)了(le)項目遷移的需求和(hé)照顧熟悉它的用戶。
和(hé)無渲染組件的對比
在組件插槽一(yī)章(zhāng)中,我們讨論過了(le)基于作(zuò)用域插槽的無渲染組件。我們甚至用它實現了(le)一(yī)樣的鼠标追蹤器示例。
組合式函數(shù)相對于無渲染組件的主要(yào)優勢是:組合式函數(shù)不會産生額外(wài)的組件實例開(kāi)銷。當在整個應用中使用時(shí),由無渲染組件産生的額外(wài)組件實例會帶來無法忽視(shì)的性能(néng)開(kāi)銷。
我們推薦在純邏輯複用時(shí)使用組合式函數(shù),在需要(yào)同時(shí)複用邏輯和(hé)視(shì)圖布局時(shí)使用無渲染組件。
和(hé) React Hooks 的對比
如(rú)果你有 React 的開(kāi)發經驗,你可(kě)能(néng)注意到組合式函數(shù)和(hé)自(zì)定義 React hooks 非常相似。組合式 API 的一(yī)部分靈感正來自(zì)于 React hooks,Vue 的組合式函數(shù)也的确在邏輯組合能(néng)力上(shàng)與 React hooks 相近。然而,Vue 的組合式函數(shù)是基于 Vue 細粒度的響應性系統,這(zhè)和(hé) React hooks 的執行(xíng)模型有本質上(shàng)的不同。這(zhè)一(yī)話題在組合式 API 的常見(jiàn)問(wèn)題中有更細緻的讨論。
網站建設開(kāi)發|APP設計開(kāi)發|小程序建設開(kāi)發