為什么說(shuō)組合式函數(shù)是 Vue3 中最棒的特性之一 ?
組合式函數(shù)(Composition API)是 Vue3 中引入的一個(gè)重要特性,它可以說(shuō)是 Vue3 中最棒的特性之一,主要有以下幾個(gè)原因:
- 更好的代碼組織
組合式函數(shù)讓組件邏輯可以通過(guò)組合多個(gè)小的單元函數(shù)來(lái)組織,每個(gè)函數(shù)負(fù)責(zé)一個(gè)具體的功能。這種函數(shù)式的編程范式可以讓代碼更加清晰易懂。
- 更好的代碼復(fù)用
組合函數(shù)可以很容易地在多個(gè)組件中復(fù)用,使得開(kāi)發(fā)者可以抽象出通用的業(yè)務(wù)邏輯作為可復(fù)用的邏輯單元。這避免了同樣邏輯代碼的重復(fù)。
- 更好的類(lèi)型推導(dǎo)
通過(guò) TypeScript 的類(lèi)型系統(tǒng),組合函數(shù)可以提供更準(zhǔn)確的代碼提示,提高開(kāi)發(fā)效率。
- 更好的邏輯抽象
組合函數(shù)讓組件只需要關(guān)注自身的 UI 展示,通過(guò)組合函數(shù)將邏輯抽象成可重用的代碼,使組件代碼更加清晰和聚合。
- 更好的面向切面編程
組合函數(shù)天然適合面向切面編程,可以更方便地處理一些與組件邏輯無(wú)關(guān)的橫切關(guān)注點(diǎn),如日志、緩存等。
- 更好的邏輯復(fù)用和代碼組織
總之,組合式函數(shù)為 Vue 帶來(lái)了函數(shù)式編程的思想,可以幫助開(kāi)發(fā)者寫(xiě)出更優(yōu)雅的代碼,是 Vue3 相比 Vue2 最大的進(jìn)步之一。它讓 Vue 的編程體驗(yàn)更接近 React Hooks。
什么是“組合式函數(shù)”?
在 Vue 應(yīng)用的概念中,“組合式函數(shù)”(Composables) 是一個(gè)利用 Vue 的組合式 API 來(lái)封裝和復(fù)用有狀態(tài)邏輯的函數(shù)。
當(dāng)構(gòu)建前端應(yīng)用時(shí),我們常常需要復(fù)用公共任務(wù)的邏輯。例如為了在不同地方格式化時(shí)間,我們可能會(huì)抽取一個(gè)可復(fù)用的日期格式化函數(shù)。這個(gè)函數(shù)封裝了無(wú)狀態(tài)的邏輯:它在接收一些輸入后立刻返回所期望的輸出。復(fù)用無(wú)狀態(tài)邏輯的庫(kù)有很多,比如你可能已經(jīng)用過(guò)的 lodash 或是 date-fns。
相比之下,有狀態(tài)邏輯負(fù)責(zé)管理會(huì)隨時(shí)間而變化的狀態(tài)。一個(gè)簡(jiǎn)單的例子是跟蹤當(dāng)前鼠標(biāo)在頁(yè)面中的位置。在實(shí)際應(yīng)用中,也可能是像觸摸手勢(shì)或與數(shù)據(jù)庫(kù)的連接狀態(tài)這樣的更復(fù)雜的邏輯。
鼠標(biāo)跟蹤器示例?
如果我們要直接在組件中使用組合式 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”開(kāi)頭
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ù)也可以掛靠在所屬組件的生命周期上
// 來(lái)啟動(dòng)和卸載副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通過(guò)返回值暴露所管理的狀態(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>
如你所見(jiàn),核心邏輯完全一致,我們做的只是把它移到一個(gè)外部函數(shù)中去,并返回需要暴露的狀態(tài)。和在組件中一樣,你也可以在組合式函數(shù)中使用所有的組合式 API。現(xiàn)在,useMouse() 的功能可以在任何組件中輕易復(fù)用了。
更酷的是,你還可以嵌套多個(gè)組合式函數(shù):一個(gè)組合式函數(shù)可以調(diào)用一個(gè)或多個(gè)其他的組合式函數(shù)。這使得我們可以像使用多個(gè)組件組合成整個(gè)應(yīng)用一樣,用多個(gè)較小且邏輯獨(dú)立的單元來(lái)組合形成復(fù)雜的邏輯。實(shí)際上,這正是為什么我們決定將實(shí)現(xiàn)了這一設(shè)計(jì)模式的 API 集合命名為組合式 API。
舉例來(lái)說(shuō),我們可以將添加和清除 DOM 事件監(jiān)聽(tīng)器的邏輯也封裝進(jìn)一個(gè)組合式函數(shù)中:
// event.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// 如果你想的話,
// 也可以用字符串形式的 CSS 選擇器來(lái)尋找目標(biāo) DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
有了它,之前的 useMouse() 組合式函數(shù)可以被簡(jiǎn)化為:
// 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 }
}
每一個(gè)調(diào)用 useMouse() 的組件實(shí)例會(huì)創(chuàng)建其獨(dú)有的 x、y 狀態(tài)拷貝,因此他們不會(huì)互相影響。如果你想要在組件之間共享狀態(tài),請(qǐng)閱讀狀態(tài)管理這一章。
異步狀態(tài)示例?
useMouse() 組合式函數(shù)沒(méi)有接收任何參數(shù),因此讓我們?cè)賮?lái)看一個(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ù):
// 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>
接收響應(yīng)式狀態(tài)?
useFetch() 接收一個(gè)靜態(tài) URL 字符串作為輸入——因此它只會(huì)執(zhí)行一次 fetch 并且就此結(jié)束。如果我們想要在 URL 改變時(shí)重新 fetch 呢?為了實(shí)現(xiàn)這一點(diǎn),我們需要將響應(yīng)式狀態(tài)傳入組合式函數(shù),并讓它基于傳入的狀態(tài)來(lái)創(chuàng)建執(zhí)行操作的偵聽(tīng)器。
舉例來(lái)說(shuō),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 來(lái)重構(gòu)我們現(xiàn)有的實(shí)現(xiàn):
// 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ù)。它的工作方式類(lèi)似于 unref(),但對(duì)函數(shù)有特殊處理。
注意 toValue(url) 是在 watchEffect 回調(diào)函數(shù)的內(nèi)部調(diào)用的。這確保了在 toValue() 規(guī)范化期間訪問(wèn)的任何響應(yīng)式依賴(lài)項(xiàng)都會(huì)被偵聽(tīng)器跟蹤。
這個(gè)版本的 useFetch() 現(xiàn)在能接收靜態(tài) URL 字符串、ref 和 getter,使其更加靈活。watch effect 會(huì)立即運(yùn)行,并且會(huì)跟蹤 toValue(url) 期間訪問(wèn)的任何依賴(lài)項(xiàng)。如果沒(méi)有跟蹤到依賴(lài)項(xiàng)(例如 url 已經(jīng)是字符串),則 effect 只會(huì)運(yùn)行一次;否則,它將在跟蹤到的任何依賴(lài)項(xiàng)更改時(shí)重新運(yùn)行。
這是更新后的 useFetch(),為了便于演示,添加了人為延遲和隨機(jī)錯(cuò)誤。
想要了解更多關(guān)于Vue3 組合式函數(shù)的用法, 請(qǐng)點(diǎn)擊 《Vue3 組合式函數(shù)》。