组合式函数 - 封装和复用有状态逻辑 - vue 组件复用
组合式函数
组合式函数
在 Vue 应用的概念中,组合式函数是一个利用 Vue 组合式 API 来封装和复用有状态逻辑的函数。
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns。
相比之下,有状态逻辑负责管理会随时间而变化的状态。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。
鼠标跟踪器示例
如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:
Mouse position is at: {{ x }}, {{ y }}
但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:
// 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 } }
下面是它在组件中使用的方式:
Mouse position is at: {{ x }}, {{ y }}
如你所见,核心逻辑一点都没有被改变,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式 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
状态拷贝,因此他们不会互相影响。如果你想要在组件之间共享状态,请阅读pinia、vuex状态管理。
异步状态示例
useMouse()
组合式函数没有接收任何参数,因此让我们再来看一个需要接收一个参数的组合式函数示例。使用fetch()
在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。
Oops! Error encountered: {{ error.message }}Data loaded:{{ data }}Loading...
同样,如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:
// 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 } }
现在我们在组件里只需要:
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发生改变时,数据就会被重置,并重新请求。
unref()函数:如果参数是ref,则返回内部值,否则返回参数本身。这是一个糖函数。
val = isRef(val) ? val.value : val
约定和最佳实践
命名
组合式函数约定用驼峰命名法命名,并以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 之后仍可以保持响应性:
// 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 }}
副作用
在组合式函数中的确可以执行effect副作用(例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
- 如果你在一个应用中使用了服务器端渲染(SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:
onMounted()
。这些钩子仅会在浏览器中使用,因此可以确保能访问到 DOM。 - 确保在
onUnmounted()
时清理副作用。举个例子,如果一个组合式函数设置了一个事件监听器,它就应该在onUnmounted()
中被移除(就像我们在useMouse()
示例中看到的一样)。当然也可以像之前的useEventListener()
示例那样,使用一个组合式函数来自动帮你做这些事。
使用限制
组合式函数在 在某种程度上,你可以将这些提取出的组合式函数看作是可以相互通信的组件范围内的服务。 如果你正在使用选项式 API,组合式函数必须在 Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板: 基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。 在组件插槽一章中,我们讨论过了基于作用域插槽的无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。 组合式函数相对于无渲染组件的主要优势是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。 我们推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。 如果你有 React 的开发经验,你可能注意到组合式函数和自定义 React hook 非常相似。组合式 API 的一部分灵感正来自于 React hook,Vue 的组合式函数也的确在逻辑组合能力上与 React hook 相近。然而,Vue 的组合式函数是基于 Vue 细粒度的响应性系统,这和 React hook 的执行模型有本质上的不同。这一话题在组合式 API 的常见问题中有更细致的讨论。在选项式 API 中使用组合式函数
setup()
中调用。且其返回的绑定必须在setup()
中返回,以便暴露给this
及其模板:与其他技巧的比较
相比于 Mixin
相比于无渲染组件
相比于 React Hook
鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com
图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!