做(zuò)自(zì)由與創造的先行(xíng)者

組合式函數(shù)

Vue.js中文手冊

此章(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)發
下(xià)一(yī)篇:自(zì)定義指令
上(shàng)一(yī)篇:異步組件