组合式函数
在Vue应用的概念中,“组合式函数”(Composables)是一个利用Vue的组合式API来封装和复用有状态逻辑的函数。
当构建前端应用时,常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。
相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或数据库的连接状态这样的更复杂的逻辑。
鼠标跟踪器示例
如果我们要直接在组件中使用组合式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()的功能可以在任何组件中轻易复用了。
更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。
举例来说:我们可以将添加和清除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 }
}
每一个调用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>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>
如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。可以把它抽取成一个组合式函数。
// 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字符串作为输入——因此它会执行一次fetch并且就此结束。如果我们想要在URL改变时重新fetch呢?为了实现这一点,我们需要将响应式状态传入组合式函数,并让它基于传入的状态来创建执行操作的侦听器。
举例来说,useFetch() 应该能够接收一个ref:
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 这将会重新触发 fetch
url.value = '/new-url'
或者接收一个getter函数:
// 当 props.id 改变时重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)
可以用watchEffect() 和 toValue() API来重构我们现有的实现:
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// reset state before fetching..
data.value = null
error.value = null
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
watchEffect(() => {
fetchData()
})
return { data, error }
}
toValue()是一个在3.3版本中新增的API。它的设计目的是将 ref 或 getter 规范化为值。如果参数是ref,它返回ref的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。它的工作方式类似于unref(),但对函数有特殊处理。
注意toValue(url)是在watchEffect 回调函数的内部调用的。这确保了在toValue() 规范化期间访问的任何响应式依赖项都会被侦听器跟踪。
这个版本的 useFetch() 现在能接收静态URL字符串、ref和getter,使其更加灵活。watcheffect会立即运行,并且会跟踪 toValue(url) 期间访问的任何依赖项。如果没有跟踪到依赖项(例如 url 已经是字符串),则effect只会运行一次;否则,它将在跟踪到的任何依赖项更改时重新运行。
约定和最佳实践
命名
组合式函数约定用驼峰命名法命名,并以”use“作为开头。
输入参数
即便不依赖于ref或getter的响应性,组合式函数也可以接收它们作为参数。如果你正在编写一个可能被其他开发者使用的组合式函数,最好处理一下输入参数是ref或getter而非原始值的情况。可以利用toValue() 工具函数来实现:
import { toValue } from 'vue'
function useFeature(maybeRefOrGetter) {
// 如果 maybeRefOrGetter 是一个 ref 或 getter,
// 将返回它的规范化值。
// 否则原样返回。
const value = toValue(maybeRefOrGetter)
}
如果你的组合式函数在输入参数是ref或getter的情况下创建了响应式effect,为了让它能够被正确追踪,请确保要么使用watch()显示地监视ref或getter,要么在watchEffect()中调用toValue()。
返回值
你可以已经注意到了,我们一直在组合式函数中使用ref()而不是reactive()。推荐地约定是组合式函数始终返回一个包含多个ref的普通的非响应式对象,这样该对象在组件中被结构为ref之后仍可以保持响应性:
// 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: {{ mouse.x }}, {{ mouse.y }}
副作用
在组合式函数中的的确可以执行副作用(例如:添加DOM事件监听器或者请求数据),但请注意以下规则:
如果你的应用用到了服务端渲染(SSR),请确保在组件挂载后才调用的声明周期钩子中执行DOM相关的副作用。例如:onMounted()。这些钩子仅会在浏览器中被调用,因此可以确保能访问到DOM。
确保在onUnmounted()时清理副作用。举例来说:如果一个组合式函数设置了一个事件监听器,它就应该在onUnmounted()中被移除。当然也可以像之前的 useEventListener()示例那样,使用一个组合式函数来自动帮你做这些事。
使用限制
组合式函数只能在 <script setup> 或 setup()钩子中被调用。 在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像onMounted()这样的声明周期钩子中调用它们。
这些限制很重要,因为这些是Vue用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:
1.将生命周期钩子注册到该组件实例上
2.将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
提示
<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中使用组合式函数
如果你正在使用选项式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)
}
// ...其他选项
}