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