深入探討Vue?3中的組合式函數(shù)編程方式
什么是組合式函數(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?,F(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 } }
TIP
每一個(gè)調(diào)用useMouse()
的組件實(shí)例會(huì)創(chuàng)建其獨(dú)有的x
、y
狀態(tài)拷貝,因此他們不會(huì)互相影響。
異步狀態(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>
useFetch()
接收一個(gè)靜態(tài)的 URL 字符串作為輸入,所以它只執(zhí)行一次請(qǐng)求,然后就完成了。但如果我們想讓它在每次 URL 變化時(shí)都重新請(qǐng)求呢?那我們可以讓它同時(shí)允許接收 ref 作為參數(shù):
// 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)求之前重設(shè)狀態(tài)... data.value = null error.value = null // unref() 解包可能為 ref 的值 fetch(unref(url)) .then((res) => res.json()) .then((json) => (data.value = json)) .catch((err) => (error.value = err)) } if (isRef(url)) { // 若輸入的 URL 是一個(gè) ref,那么啟動(dòng)一個(gè)響應(yīng)式的請(qǐng)求 watchEffect(doFetch) } else { // 否則只請(qǐng)求一次 // 避免監(jiān)聽(tīng)器的額外開(kāi)銷 doFetch() } return { data, error } }
這個(gè)版本的useFetch()
現(xiàn)在同時(shí)可以接收靜態(tài)的 URL 字符串和 URL 字符串的 ref。當(dāng)通過(guò)isRef()檢測(cè)到 URL 是一個(gè)動(dòng)態(tài) ref 時(shí),它會(huì)使用watchEffect()啟動(dòng)一個(gè)響應(yīng)式的 effect。該 effect 會(huì)立刻執(zhí)行一次,并在此過(guò)程中將 URL 的 ref 作為依賴進(jìn)行跟蹤。當(dāng) URL 的 ref 發(fā)生改變時(shí),數(shù)據(jù)就會(huì)被重置,并重新請(qǐng)求。
約定和最佳實(shí)踐
? 命名?
組合式函數(shù)約定用駝峰命名法命名,并以“use”作為開(kāi)頭。
輸入?yún)?shù)?
盡管其響應(yīng)性不依賴 ref,組合式函數(shù)仍可接收 ref 參數(shù)。如果編寫的組合式函數(shù)會(huì)被其他開(kāi)發(fā)者使用,你最好在處理輸入?yún)?shù)時(shí)兼容 ref 而不只是原始的值。unref()工具函數(shù)會(huì)對(duì)此非常有幫助:
import { unref } from 'vue' function useFeature(maybeRef) { // 若 maybeRef 確實(shí)是一個(gè) ref,它的 .value 會(huì)被返回 // 否則,maybeRef 會(huì)被原樣返回 const value = unref(maybeRef) }
如果你的組合式函數(shù)在接收 ref 為參數(shù)時(shí)會(huì)產(chǎn)生響應(yīng)式 effect,請(qǐng)確保使用watch()
顯式地監(jiān)聽(tīng)此 ref,或者在watchEffect()
中調(diào)用unref()
來(lái)進(jìn)行正確的追蹤。
返回值?
你可能已經(jīng)注意到了,我們一直在組合式函數(shù)中使用ref()
而不是reactive()
。我們推薦的約定是組合式函數(shù)始終返回一個(gè)包含多個(gè) ref 的普通的非響應(yīng)式對(duì)象,這樣該對(duì)象在組件中被解構(gòu)為 ref 之后仍可以保持響應(yīng)性:
js
// x 和 y 是兩個(gè) ref const { x, y } = useMouse()
從組合式函數(shù)返回一個(gè)響應(yīng)式對(duì)象會(huì)導(dǎo)致在對(duì)象解構(gòu)過(guò)程中丟失與組合式函數(shù)內(nèi)狀態(tài)的響應(yīng)性連接。與之相反,ref 則可以維持這一響應(yīng)性連接。
如果你更希望以對(duì)象屬性的形式來(lái)使用組合式函數(shù)中返回的狀態(tài),你可以將返回的對(duì)象用reactive()
包裝一次,這樣其中的 ref 會(huì)被自動(dòng)解包,例如:
const mouse = reactive(useMouse()) // mouse.x 鏈接到了原來(lái)的 x ref console.log(mouse.x)
Mouse position is at: {<!--{C}%3C!%2D%2D%20%2D%2D%3E-->{ mouse.x }}, {<!--{C}%3C!%2D%2D%20%2D%2D%3E-->{ mouse.y }}
副作用?
在組合式函數(shù)中的確可以執(zhí)行副作用 (例如:添加 DOM 事件監(jiān)聽(tīng)器或者請(qǐng)求數(shù)據(jù)),但請(qǐng)注意以下規(guī)則:
- 如果你的應(yīng)用用到了服務(wù)端渲染(SSR),請(qǐng)確保在組件掛載后才調(diào)用的生命周期鉤子中執(zhí)行 DOM 相關(guān)的副作用,例如:
onMounted()
。這些鉤子僅會(huì)在瀏覽器中被調(diào)用,因此可以確保能訪問(wèn)到 DOM。 - 確保在
onUnmounted()
時(shí)清理副作用。舉例來(lái)說(shuō),如果一個(gè)組合式函數(shù)設(shè)置了一個(gè)事件監(jiān)聽(tīng)器,它就應(yīng)該在onUnmounted()
中被移除 (就像我們?cè)?code>useMouse()示例中看到的一樣)。當(dāng)然也可以像之前的useEventListener()
示例那樣,使用一個(gè)組合式函數(shù)來(lái)自動(dòng)幫你做這些事。
使用限制?
組合式函數(shù)在<script setup>
或setup()
鉤子中,應(yīng)始終被同步地調(diào)用。在某些場(chǎng)景下,你也可以在像onMounted()
這樣的生命周期鉤子中使用他們。
這個(gè)限制是為了讓 Vue 能夠確定當(dāng)前正在被執(zhí)行的到底是哪個(gè)組件實(shí)例,只有能確認(rèn)當(dāng)前組件實(shí)例,才能夠:
- 將生命周期鉤子注冊(cè)到該組件實(shí)例上
- 將計(jì)算屬性和監(jiān)聽(tīng)器注冊(cè)到該組件實(shí)例上,以便在該組件被卸載時(shí)停止監(jiān)聽(tīng),避免內(nèi)存泄漏。
TIP
<script setup>
是唯一在調(diào)用await
之后仍可調(diào)用組合式函數(shù)的地方。編譯器會(huì)在異步操作之后自動(dòng)為你恢復(fù)當(dāng)前的組件實(shí)例。
通過(guò)抽取組合式函數(shù)改善代碼結(jié)構(gòu)
抽取組合式函數(shù)不僅是為了復(fù)用,也是為了代碼組織。隨著組件復(fù)雜度的增高,你可能會(huì)最終發(fā)現(xiàn)組件多得難以查詢和理解。組合式 API 會(huì)給予你足夠的靈活性,讓你可以基于邏輯問(wèn)題將組件代碼拆分成更小的函數(shù):
<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ù)看作是可以相互通信的組件范圍內(nèi)的服務(wù)。
選項(xiàng)式API中使用組合式函數(shù)?
如果你正在使用選項(xiàng)式 API,組合式函數(shù)必須在setup()
中調(diào)用。且其返回的綁定必須在setup()
中返回,以便暴露給this
及其模板:
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() 暴露的屬性可以在通過(guò) `this` 訪問(wèn)到 console.log(this.x) } // ...其他選項(xiàng) }
與其他模式的比較
和Mixin的對(duì)比?
Vue 2 的用戶可能會(huì)對(duì)mixins選項(xiàng)比較熟悉。它也讓我們能夠把組件邏輯提取到可復(fù)用的單元里。然而 mixins 有三個(gè)主要的短板:
- 不清晰的數(shù)據(jù)來(lái)源:當(dāng)使用了多個(gè) mixin 時(shí),實(shí)例上的數(shù)據(jù)屬性來(lái)自哪個(gè) mixin 變得不清晰,這使追溯實(shí)現(xiàn)和理解組件行為變得困難。這也是我們推薦在組合式函數(shù)中使用 ref + 解構(gòu)模式的理由:讓屬性的來(lái)源在消費(fèi)組件時(shí)一目了然。
- 命名空間沖突:多個(gè)來(lái)自不同作者的 mixin 可能會(huì)注冊(cè)相同的屬性名,造成命名沖突。若使用組合式函數(shù),你可以通過(guò)在解構(gòu)變量時(shí)對(duì)變量進(jìn)行重命名來(lái)避免相同的鍵名。
- 隱式的跨 mixin 交流:多個(gè) mixin 需要依賴共享的屬性名來(lái)進(jìn)行相互作用,這使得它們隱性地耦合在一起。而一個(gè)組合式函數(shù)的返回值可以作為另一個(gè)組合式函數(shù)的參數(shù)被傳入,像普通函數(shù)那樣。
基于上述理由,我們不再推薦在 Vue 3 中繼續(xù)使用 mixin。保留該功能只是為了項(xiàng)目遷移的需求和照顧熟悉它的用戶。
和無(wú)渲染組件的對(duì)比?
在組件插槽一章中,我們討論過(guò)了基于作用域插槽的無(wú)渲染組件。我們甚至用它實(shí)現(xiàn)了一樣的鼠標(biāo)追蹤器示例。
組合式函數(shù)相對(duì)于無(wú)渲染組件的主要優(yōu)勢(shì)是:組合式函數(shù)不會(huì)產(chǎn)生額外的組件實(shí)例開(kāi)銷。當(dāng)在整個(gè)應(yīng)用中使用時(shí),由無(wú)渲染組件產(chǎn)生的額外組件實(shí)例會(huì)帶來(lái)無(wú)法忽視的性能開(kāi)銷。
我們推薦在純邏輯復(fù)用時(shí)使用組合式函數(shù),在需要同時(shí)復(fù)用邏輯和視圖布局時(shí)使用無(wú)渲染組件。
和React Hooks的對(duì)比?
如果你有 React 的開(kāi)發(fā)經(jīng)驗(yàn),你可能注意到組合式函數(shù)和自定義 React hooks 非常相似。組合式 API 的一部分靈感正來(lái)自于 React hooks,Vue 的組合式函數(shù)也的確在邏輯組合能力上與 React hooks 相近。然而,Vue 的組合式函數(shù)是基于 Vue 細(xì)粒度的響應(yīng)性系統(tǒng),這和 React hooks 的執(zhí)行模型有本質(zhì)上的不同。
到此這篇關(guān)于深入探討Vue 3中的組合式函數(shù)編程方式的文章就介紹到這了,更多相關(guān)Vue組合式函數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用iView Upload 組件實(shí)現(xiàn)手動(dòng)上傳圖片的示例代碼
這篇文章主要介紹了使用iView Upload 組件實(shí)現(xiàn)手動(dòng)上傳圖片的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10vue實(shí)現(xiàn)簡(jiǎn)單的星級(jí)評(píng)分組件源碼
這篇文章主要介紹了vue星級(jí)評(píng)分組件源碼,代碼簡(jiǎn)單易懂非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-11-11vue中v-model動(dòng)態(tài)生成的實(shí)例詳解
這篇文章主要介紹了vue中v-model動(dòng)態(tài)生成的實(shí)例詳解的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-10-10vue-drag-resize與輸入框/文本框沖突問(wèn)題
這篇文章主要介紹了vue-drag-resize與輸入框/文本框沖突問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04vuejs開(kāi)發(fā)組件分享之H5圖片上傳、壓縮及拍照旋轉(zhuǎn)的問(wèn)題處理
這篇文章主要介紹了vuejs開(kāi)發(fā)組件分享之H5圖片上傳、壓縮及拍照旋轉(zhuǎn)的問(wèn)題處理,需要的朋友可以參考下2017-03-03關(guān)于uniapp的高級(jí)表單組件mosowe-form
這篇文章主要介紹了關(guān)于uniapp的高級(jí)表單組件mosowe-form,由于一些表單標(biāo)簽改來(lái)改去比較繁瑣,重復(fù)性很多,且樣式布局啥的幾乎萬(wàn)變不離其中,為了偷懶,開(kāi)發(fā)了mosowe-form及mosowe-table兩款高級(jí)組件,需要的朋友可以參考下2023-04-04vue如何利用store實(shí)現(xiàn)兩個(gè)平行組件間的傳值
這篇文章主要介紹了vue如何利用store實(shí)現(xiàn)兩個(gè)平行組件間的傳值,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04