In the concept of Vue application, "combinable function" (Composables) is a function that uses Vue's combined API to encapsulate and reuse stateful logic.
When building front-end applications, we often need to reuse the logic of common tasks. For example, in order to format time in different places, we might extract a reusable date formatting function. This function encapsulates stateless logic: it takes some input and returns the desired output immediately. There are many libraries that reuse stateless logic, such as lodash or date-fns that you may have used.
In contrast, stateful logic is responsible for managing state that changes over time. A simple example is tracking the current mouse position on the page. In a real application, it could also be more complex logic like touch gestures or connection status to a database.
If we were to use the combined API directly in the component to implement the mouse tracking function, it would look like this:
<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>
However, if we want to What about reusing the same logic in multiple components? We can extract this logic into an external file in the form of a combined function:
// 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 } }
Here's how it is used in the component:
<script setup> import { useMouse } from './mouse.js' const { x, y } = useMouse() </script> <template>Mouse position is at: {{ x }}, {{ y }}</template>
As you can see, the core logic is exactly the same, all we do is move it to an external function and return the state that needs to be exposed. Just like in components, you can use all composition APIs in composition functions. Now, the functionality of useMouse()
can be easily reused in any component.
What’s even cooler is that you can also nest multiple combined functions: a combined function can call one or more other combined functions. This allows us to combine multiple smaller and logically independent units to form complex logic, just like multiple components are combined to form an entire application. In fact, that's exactly why we decided to call the collection of APIs that implement this design pattern Composable APIs.
For example, we can encapsulate the logic of adding and clearing DOM event listeners into a combined function:
// 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)) }
With it, the previous useMouse()
The combined function can be simplified to:
// 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
Each component instance that calls useMouse()
will create its own unique x
, y
status copies, so they will not affect each other.
useMouse()
The combined function does not receive any parameters, so let's look at another example of a combined function that needs to receive one parameter. When making asynchronous data requests, we often need to handle different states: loading, loading success and loading failure.
<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 }}
It would be too cumbersome to repeat this pattern in every component that needs to get data. Let's extract it into a combined function:
// 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 } }
Now we only need in the component:
<script setup> import { useFetch } from './fetch.js' const { data, error } = useFetch('...') </script>
useFetch()
Receive a static URL string as input, so it only performs the request once and then it's done. But what if we want it to re-request every time the URL changes? Then we can make it allow receiving ref as a parameter at the same time:
// 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 } }
This version of useFetch()
can now receive both static URL strings and URL string refs. When it detects that the URL is a dynamic ref via isRef(), it will use watchEffect() to start a reactive effect. The effect will be executed immediately, and in the process the URL's ref will be tracked as a dependency. When the URL's ref changes, the data is reset and the request is made again.
The convention for combined functions is to use camel case naming and start with "use".
Although its responsiveness does not depend on ref, composed functions can still receive ref parameters. If you're writing a composed function that will be used by other developers, you'd better handle input parameters with ref compatibility rather than just raw values. The unref() utility function will be very helpful for this:
import { unref } from 'vue' function useFeature(maybeRef) { // 若 maybeRef 确实是一个 ref,它的 .value 会被返回 // 否则,maybeRef 会被原样返回 const value = unref(maybeRef) }
If your combined function will produce a reactive effect when receiving ref as a parameter, please make sure to use watch()
to show Listen to this ref in a style, or call unref()
in watchEffect()
for correct tracking.
You may have noticed that we have been using ref()
instead of reactive()
in combined functions. Our recommended convention is that composed functions always return a plain, non-reactive object containing multiple refs, so that the object remains responsive after being destructured into refs in the component:
js
// x 和 y 是两个 ref const { x, y } = useMouse()
Returning a reactive object from a composed function causes the reactive connection to the state within the composed function to be lost during object destructuring. In contrast, ref maintains this responsive connection.
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用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 的执行模型有本质上的不同。
The above is the detailed content of What is the combined function programming method in Vue3. For more information, please follow other related articles on the PHP Chinese website!