做自(zì)由與創造的先行者

組合式函數

Vue.js中文(wén)手冊

此章節假設你(nǐ)已經對(duì)組合式 API 有了(le)基本的了(le)解。如果你(nǐ)隻學習過選項式 API,你(nǐ)可以使用(yòng)左側邊欄上(shàng)方的切換按鈕将 API 風(fēng)格切換爲組合式 API 後,重新閱讀響應性基礎和(hé)生命周期鈎子兩個章節。

什(shén)麽是“組合式函數”? ​

在 Vue 應用(yòng)的概念中,“組合式函數”(Composables) 是一個利用(yòng) Vue 的組合式 API 來(lái)封裝和(hé)複用(yòng)有狀态邏輯的函數。

當構建前端應用(yòng)時(shí),我們常常需要複用(yòng)公共任務的邏輯。例如爲了(le)在不同地方格式化時(shí)間,我們可能(néng)會(huì)抽取一個可複用(yòng)的日期格式化函數。這(zhè)個函數封裝了(le)無狀态的邏輯:它在接收一些(xiē)輸入後立刻返回所期望的輸出。複用(yòng)無狀态邏輯的庫有很(hěn)多,比如你(nǐ)可能(néng)已經用(yòng)過的 lodash 或是 date-fns。

相比之下(xià),有狀态邏輯負責管理(lǐ)會(huì)随時(shí)間而變化的狀态。一個簡單的例子是跟蹤當前鼠标在頁面中的位置。在實際應用(yòng)中,也(yě)可能(néng)是像觸摸手勢或與數據庫的連接狀态這(zhè)樣的更複雜(zá)的邏輯。

鼠标跟蹤器示例 ​

如果我們要直接在組件中使用(yòng)組合式 API 實現(xiàn)鼠标跟蹤功能(néng),它會(huì)是這(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)是,如果我們想在多個組件中複用(yòng)這(zhè)個相同的邏輯呢(ne)?我們可以把這(zhè)個邏輯以一個組合式函數的形式提取到(dào)外(wài)部文(wén)件中:

js

// mouse.js

import { ref, onMounted, onUnmounted } from 'vue'

// 按照慣例,組合式函數名以“use”開(kāi)頭

export function useMouse() {

// 被組合式函數封裝和(hé)管理(lǐ)的狀态

const x = ref(0)

const y = ref(0)

// 組合式函數可以随時(shí)更改其狀态。

function update(event) {

x.value = event.pageX

y.value = event.pageY

}

// 一個組合式函數也(yě)可以挂靠在所屬組件的生命周期上(shàng)

// 來(lái)啓動和(hé)卸載副作(zuò)用(yòng)

onMounted(() => window.addEventListener('mousemove', update))

onUnmounted(() => window.removeEventListener('mousemove', update))

// 通過返回值暴露所管理(lǐ)的狀态

return { x, y }

}

下(xià)面是它在組件中使用(yòng)的方式:

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

如你(nǐ)所見,核心邏輯完全一緻,我們做的隻是把它移到(dào)一個外(wài)部函數中去,并返回需要暴露的狀态。和(hé)在組件中一樣,你(nǐ)也(yě)可以在組合式函數中使用(yòng)所有的組合式 API。現(xiàn)在,useMouse() 的功能(néng)可以在任何組件中輕易複用(yòng)了(le)。

更酷的是,你(nǐ)還可以嵌套多個組合式函數:一個組合式函數可以調用(yòng)一個或多個其他(tā)的組合式函數。這(zhè)使得我們可以像使用(yòng)多個組件組合成整個應用(yòng)一樣,用(yòng)多個較小(xiǎo)且邏輯獨立的單元來(lái)組合形成複雜(zá)的邏輯。實際上(shàng),這(zhè)正是爲什(shén)麽我們決定将實現(xiàn)了(le)這(zhè)一設計(jì)模式的 API 集合命名爲組合式 API。

舉例來(lái)說,我們可以将添加和(hé)清除 DOM 事(shì)件監聽器的邏輯也(yě)封裝進一個組合式函數中:

js

// event.js

import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {

// 如果你(nǐ)想的話(huà),

// 也(yě)可以用(yòng)字符串形式的 CSS 選擇器來(lái)尋找目标 DOM 元素

onMounted(() => target.addEventListener(event, callback))

onUnmounted(() => target.removeEventListener(event, callback))

}

有了(le)它,之前的 useMouse() 組合式函數可以被簡化爲:

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òng) useMouse() 的組件實例會(huì)創建其獨有的 x、y 狀态拷貝,因此他(tā)們不會(huì)互相影響。如果你(nǐ)想要在組件之間共享狀态,請(qǐng)閱讀狀态管理(lǐ)這(zhè)一章。

異步狀态示例 ​

useMouse() 組合式函數沒有接收任何參數,因此讓我們再來(lái)看(kàn)一個需要接收一個參數的組合式函數示例。在做異步數據請(qǐng)求時(shí),我們常常需要處理(lǐ)不同的狀态:加載中、加載成功和(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>

如果在每個需要獲取數據的組件中都要重複這(zhè)種模式,那就太繁瑣了(le)。讓我們把它抽取成一個組合式函數:

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 }

}

現(xiàn)在我們在組件裏隻需要:

vue

<script setup>

import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')

</script>

useFetch() 接收一個靜态的 URL 字符串作(zuò)爲輸入,所以它隻執行一次請(qǐng)求,然後就完成了(le)。但(dàn)如果我們想讓它在每次 URL 變化時(shí)都重新請(qǐng)求呢(ne)?那我們可以讓它同時(shí)允許接收 ref 作(zuò)爲參數:

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() 解包可能(néng)爲 ref 的值

fetch(unref(url))

.then((res) => res.json())

.then((json) => (data.value = json))

.catch((err) => (error.value = err))

}

if (isRef(url)) {

// 若輸入的 URL 是一個 ref,那麽啓動一個響應式的請(qǐng)求

watchEffect(doFetch)

} else {

// 否則隻請(qǐng)求一次

// 避免監聽器的額外(wài)開(kāi)銷

doFetch()

}

return { data, error }

}

這(zhè)個版本的 useFetch() 現(xiàn)在同時(shí)可以接收靜态的 URL 字符串和(hé) URL 字符串的 ref。當通過 isRef() 檢測到(dào) URL 是一個動态 ref 時(shí),它會(huì)使用(yòng) watchEffect() 啓動一個響應式的 effect。該 effect 會(huì)立刻執行一次,并在此過程中将 URL 的 ref 作(zuò)爲依賴進行跟蹤。當 URL 的 ref 發生改變時(shí),數據就會(huì)被重置,并重新請(qǐng)求。

這(zhè)裏是一個升級版的 useFetch(),出于演示目的,我們人爲地設置了(le)延遲和(hé)随機報(bào)錯。

約定和(hé)最佳實踐 ​

命名 ​

組合式函數約定用(yòng)駝峰命名法命名,并以“use”作(zuò)爲開(kāi)頭。

輸入參數 ​

盡管其響應性不依賴 ref,組合式函數仍可接收 ref 參數。如果編寫的組合式函數會(huì)被其他(tā)開(kāi)發者使用(yòng),你(nǐ)最好(hǎo)在處理(lǐ)輸入參數時(shí)兼容 ref 而不隻是原始的值。unref() 工(gōng)具函數會(huì)對(duì)此非常有幫助:

js

import { unref } from 'vue'

function useFeature(maybeRef) {

// 若 maybeRef 确實是一個 ref,它的 .value 會(huì)被返回

// 否則,maybeRef 會(huì)被原樣返回

const value = unref(maybeRef)

}

如果你(nǐ)的組合式函數在接收 ref 爲參數時(shí)會(huì)産生響應式 effect,請(qǐng)确保使用(yòng) watch() 顯式地監聽此 ref,或者在 watchEffect() 中調用(yòng) unref() 來(lái)進行正确的追蹤。

返回值 ​

你(nǐ)可能(néng)已經注意到(dào)了(le),我們一直在組合式函數中使用(yòng) ref() 而不是 reactive()。我們推薦的約定是組合式函數始終返回一個包含多個 ref 的普通的非響應式對(duì)象,這(zhè)樣該對(duì)象在組件中被解構爲 ref 之後仍可以保持響應性:

js

// x 和(hé) y 是兩個 ref

const { x, y } = useMouse()

從(cóng)組合式函數返回一個響應式對(duì)象會(huì)導緻在對(duì)象解構過程中丢失與組合式函數内狀态的響應性連接。與之相反,ref 則可以維持這(zhè)一響應性連接。

如果你(nǐ)更希望以對(duì)象屬性的形式來(lái)使用(yòng)組合式函數中返回的狀态,你(nǐ)可以将返回的對(duì)象用(yòng) reactive() 包裝一次,這(zhè)樣其中的 ref 會(huì)被自(zì)動解包,例如:

js

const mouse = reactive(useMouse())

// mouse.x 鏈接到(dào)了(le)原來(lái)的 x ref

console.log(mouse.x)

template

Mouse position is at: {{ mouse.x }}, {{ mouse.y }}

副作(zuò)用(yòng) ​

在組合式函數中的确可以執行副作(zuò)用(yòng) (例如:添加 DOM 事(shì)件監聽器或者請(qǐng)求數據),但(dàn)請(qǐng)注意以下(xià)規則:

如果你(nǐ)的應用(yòng)用(yòng)到(dào)了(le)服務端渲染 (SSR),請(qǐng)确保在組件挂載後才調用(yòng)的生命周期鈎子中執行 DOM 相關的副作(zuò)用(yòng),例如:onMounted()。這(zhè)些(xiē)鈎子僅會(huì)在浏覽器中被調用(yòng),因此可以确保能(néng)訪問到(dào) DOM。

确保在 onUnmounted() 時(shí)清理(lǐ)副作(zuò)用(yòng)。舉例來(lái)說,如果一個組合式函數設置了(le)一個事(shì)件監聽器,它就應該在 onUnmounted() 中被移除 (就像我們在 useMouse() 示例中看(kàn)到(dào)的一樣)。當然也(yě)可以像之前的 useEventListener() 示例那樣,使用(yòng)一個組合式函數來(lái)自(zì)動幫你(nǐ)做這(zhè)些(xiē)事(shì)。

使用(yòng)限制 ​

組合式函數在 <script setup> 或 setup() 鈎子中,應始終被同步地調用(yòng)。在某些(xiē)場景下(xià),你(nǐ)也(yě)可以在像 onMounted() 這(zhè)樣的生命周期鈎子中使用(yòng)他(tā)們。

這(zhè)個限制是爲了(le)讓 Vue 能(néng)夠确定當前正在被執行的到(dào)底是哪個組件實例,隻有能(néng)确認當前組件實例,才能(néng)夠:

将生命周期鈎子注冊到(dào)該組件實例上(shàng)

将計(jì)算(suàn)屬性和(hé)監聽器注冊到(dào)該組件實例上(shàng),以便在該組件被卸載時(shí)停止監聽,避免内存洩漏。

TIP

<script setup> 是唯一在調用(yòng) await 之後仍可調用(yòng)組合式函數的地方。編譯器會(huì)在異步操作(zuò)之後自(zì)動爲你(nǐ)恢複當前的組件實例。

通過抽取組合式函數改善代碼結構 ​

抽取組合式函數不僅是爲了(le)複用(yòng),也(yě)是爲了(le)代碼組織。随着組件複雜(zá)度的增高(gāo),你(nǐ)可能(néng)會(huì)最終發現(xiàn)組件多得難以查詢和(hé)理(lǐ)解。組合式 API 會(huì)給予你(nǐ)足夠的靈活性,讓你(nǐ)可以基于邏輯問題将組件代碼拆分成更小(xiǎo)的函數:

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),你(nǐ)可以将這(zhè)些(xiē)提取出的組合式函數看(kàn)作(zuò)是可以相互通信的組件範圍内的服務。

在選項式 API 中使用(yòng)組合式函數 ​

如果你(nǐ)正在使用(yòng)選項式 API,組合式函數必須在 setup() 中調用(yòng)。且其返回的綁定必須在 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() 暴露的屬性可以在通過 `this` 訪問到(dào)

console.log(this.x)

}

// ...其他(tā)選項

}

與其他(tā)模式的比較 ​

和(hé) Mixin 的對(duì)比 ​

Vue 2 的用(yòng)戶可能(néng)會(huì)對(duì) mixins 選項比較熟悉。它也(yě)讓我們能(néng)夠把組件邏輯提取到(dào)可複用(yòng)的單元裏。然而 mixins 有三個主要的短闆:

不清晰的數據來(lái)源:當使用(yòng)了(le)多個 mixin 時(shí),實例上(shàng)的數據屬性來(lái)自(zì)哪個 mixin 變得不清晰,這(zhè)使追溯實現(xiàn)和(hé)理(lǐ)解組件行爲變得困難。這(zhè)也(yě)是我們推薦在組合式函數中使用(yòng) ref + 解構模式的理(lǐ)由:讓屬性的來(lái)源在消費組件時(shí)一目了(le)然。

命名空(kōng)間沖突:多個來(lái)自(zì)不同作(zuò)者的 mixin 可能(néng)會(huì)注冊相同的屬性名,造成命名沖突。若使用(yòng)組合式函數,你(nǐ)可以通過在解構變量時(shí)對(duì)變量進行重命名來(lái)避免相同的鍵名。

隐式的跨 mixin 交流:多個 mixin 需要依賴共享的屬性名來(lái)進行相互作(zuò)用(yòng),這(zhè)使得它們隐性地耦合在一起。而一個組合式函數的返回值可以作(zuò)爲另一個組合式函數的參數被傳入,像普通函數那樣。

基于上(shàng)述理(lǐ)由,我們不再推薦在 Vue 3 中繼續使用(yòng) mixin。保留該功能(néng)隻是爲了(le)項目遷移的需求和(hé)照顧熟悉它的用(yòng)戶。

和(hé)無渲染組件的對(duì)比 ​

在組件插槽一章中,我們讨論過了(le)基于作(zuò)用(yòng)域插槽的無渲染組件。我們甚至用(yòng)它實現(xiàn)了(le)一樣的鼠标追蹤器示例。

組合式函數相對(duì)于無渲染組件的主要優勢是:組合式函數不會(huì)産生額外(wài)的組件實例開(kāi)銷。當在整個應用(yòng)中使用(yòng)時(shí),由無渲染組件産生的額外(wài)組件實例會(huì)帶來(lái)無法忽視(shì)的性能(néng)開(kāi)銷。

我們推薦在純邏輯複用(yòng)時(shí)使用(yòng)組合式函數,在需要同時(shí)複用(yòng)邏輯和(hé)視(shì)圖布局時(shí)使用(yòng)無渲染組件。

和(hé) React Hooks 的對(duì)比 ​

如果你(nǐ)有 React 的開(kāi)發經驗,你(nǐ)可能(néng)注意到(dào)組合式函數和(hé)自(zì)定義 React hooks 非常相似。組合式 API 的一部分靈感正來(lái)自(zì)于 React hooks,Vue 的組合式函數也(yě)的确在邏輯組合能(néng)力上(shàng)與 React hooks 相近。然而,Vue 的組合式函數是基于 Vue 細粒度的響應性系統,這(zhè)和(hé) React hooks 的執行模型有本質上(shàng)的不同。這(zhè)一話(huà)題在組合式 API 的常見問題中有更細緻的讨論。

網站(zhàn)建設開(kāi)發|APP設計(jì)開(kāi)發|小(xiǎo)程序建設開(kāi)發
下(xià)一篇:自(zì)定義指令
上(shàng)一篇:異步組件