在 Vue 應用程式的概念中,「組合式函數」(Composables) 是利用 Vue 的組合式 API 來封裝和重複使用有狀態邏輯的函數。
當建構前端應用時,我們常常需要重複使用公共任務的邏輯。例如為了在不同地方格式化時間,我們可能會抽取一個可重複使用的日期格式化函數。這個函數封裝了無狀態的邏輯:它在接收一些輸入後立刻回傳所期望的輸出。重複使用無狀態邏輯的函式庫有很多,例如你可能已經用過的lodash或是date-fns。
相較之下,有狀態邏輯負責管理會隨時間而改變的狀態。一個簡單的例子是追蹤當前滑鼠在頁面中的位置。在實際應用中,也可能是像是觸控手勢或與資料庫的連線狀態這樣的更複雜的邏輯。
如果我們要直接在元件中使用組合式API 實作滑鼠追蹤功能,它會是這樣的:
<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>
但是,如果我們想在多個元件中復用這個相同的邏輯呢?我們可以把這個邏輯以一個組合式函數的形式提取到外部檔案中:
// mouse.js import { ref, onMounted, onUnmounted } from 'vue' // 按照惯例,组合式函数名以“use”开头 export function useMouse() { // 被组合式函数封装和管理的状态 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)) // 通过返回值暴露所管理的状态 return { x, y } }
下面是它在元件中使用的方式:
<script setup> import { useMouse } from './mouse.js' const { x, y } = useMouse() </script> <template>Mouse position is at: {{ x }}, {{ y }}</template>
如你所見,核心邏輯完全一致,我們所做的只是把它移到一個外部函數中去,並返回需要暴露的狀態。和在元件中一樣,你也可以在組合式函數中使用所有的組合 API。現在,useMouse()
的功能可以在任何元件中輕易重複使用了。
更酷的是,你還可以嵌套多個組合式函數:一個組合式函數可以呼叫一個或多個其他的組合式函數。這使得我們可以像使用多個元件組合成整個應用一樣,用多個較小且邏輯獨立的單元來組合形成複雜的邏輯。實際上,這正是為什麼我們決定將實作了這個設計模式的 API 集合命名為組合 API。
舉例來說,我們可以將新增和清除DOM 事件監聽器的邏輯也封裝進一個組合式函數中:
// 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()
組合式函數可以簡化為:
// 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
每一個呼叫useMouse()
的元件實例會建立其獨特的x
、y
狀態拷貝,因此他們不會互相影響。
useMouse()
組合式函數沒有接收任何參數,因此讓我們再來看一個需要接收一個參數的組合式函數範例。在做非同步資料請求時,我們常常需要處理不同的狀態:載入中、載入成功和載入失敗。
<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 class="brush:php;toolbar:false">{{ data }}
如果在每個需要取得資料的元件中都要重複這種模式,那就太繁瑣了。讓我們把它抽取成一個組合式函數:
// 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 } }
現在我們在元件裡只需要:
<script setup> import { useFetch } from './fetch.js' const { data, error } = useFetch('...') </script>
useFetch()
接收一個靜態的URL 字串作為輸入,所以它只執行一次請求,然後就完成了。但如果我們想讓它在每次 URL 變更時都重新要求呢?那我們可以讓它同時允許接收 ref 作為參數:
// fetch.js import { ref, isRef, unref, watchEffect } from 'vue' export function useFetch(url) { const data = ref(null) const error = ref(null) function doFetch() { // 在请求之前重设状态... 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 是一个 ref,那么启动一个响应式的请求 watchEffect(doFetch) } else { // 否则只请求一次 // 避免监听器的额外开销 doFetch() } return { data, error } }
這個版本的useFetch()
現在同時可以接收靜態的 URL 字串和 URL 字串的 ref。當透過isRef()偵測到 URL 是動態 ref 時,它會使用watchEffect()啟動一個響應式的 effect。該 effect 會立刻執行一次,並在過程中將 URL 的 ref 作為依賴項進行追蹤。當 URL 的 ref 改變時,資料就會被重置,並重新要求。
#組合式函數約定以駝峰命名法命名,並以「use」作為開頭。
儘管其響應性不依賴 ref,組合式函數仍可接收 ref 參數。如果編寫的組合式函數會被其他開發者使用,你最好在處理輸入參數時相容於 ref 而不只是原始的值。 unref()工具函數會對此非常有幫助:
import { unref } from 'vue' function useFeature(maybeRef) { // 若 maybeRef 确实是一个 ref,它的 .value 会被返回 // 否则,maybeRef 会被原样返回 const value = unref(maybeRef) }
如果你的組合函數在接收ref 為參數時會產生響應式effect,請確保使用watch()
顯式地監聽此ref,或在watchEffect()
中呼叫unref()
來進行正確的追蹤。
你可能已經注意到了,我們一直在組合式函數中使用ref()
而不是reactive()
。我們推薦的約定是組合式函數總是會傳回一個包含多個ref 的普通的非響應式對象,這樣該物件在元件中被解構為ref 之後仍可以保持響應性:
js
// x 和 y 是两个 ref const { x, y } = useMouse()
從組合式函數傳回一個響應式物件會導致在物件解構過程中遺失與組合式函數內狀態的響應性連接。與之相反,ref 則可以維持此響應性連結。
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用reactive()
包装一次,这样其中的 ref 会被自动解包,例如:
const mouse = reactive(useMouse()) // mouse.x 链接到了原来的 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 }}
在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
如果你的应用用到了服务端渲染(SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:onMounted()
。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。
确保在onUnmounted()
时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在onUnmounted()
中被移除 (就像我们在useMouse()
示例中看到的一样)。当然也可以像之前的useEventListener()
示例那样,使用一个组合式函数来自动帮你做这些事。
组合式函数在<script setup>
或setup()
钩子中,应始终被同步地调用。在某些场景下,你也可以在像onMounted()
这样的生命周期钩子中使用他们。
这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:
将生命周期钩子注册到该组件实例上
将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
TIP
<script setup>
是唯一在调用await
之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。
抽取组合式函数不仅是为了复用,也是为了代码组织。随着组件复杂度的增高,你可能会最终发现组件多得难以查询和理解。组合式 API 会给予你足够的灵活性,让你可以基于逻辑问题将组件代码拆分成更小的函数:
<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>
在某种程度上,你可以将这些提取出的组合式函数看作是可以相互通信的组件范围内的服务。
如果你正在使用选项式 API,组合式函数必须在setup()
中调用。且其返回的绑定必须在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() 暴露的属性可以在通过 `this` 访问到 console.log(this.x) } // ...其他选项 }
Vue 2 的用户可能会对mixins选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:
不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。
命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。
隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。
基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。
在组件插槽一章中,我们讨论过了基于作用域插槽的无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。
组合式函数相对于无渲染组件的主要优势是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。
我们推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。
如果你有 React 的开发经验,你可能注意到组合式函数和自定义 React hooks 非常相似。组合式 API 的一部分灵感正来自于 React hooks,Vue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而,Vue 的组合式函数是基于 Vue 细粒度的响应性系统,这和 React hooks 的执行模型有本质上的不同。
以上是Vue3中的組合式函數程式設計方法是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!