Vue3中的组合式函数编程方法是什么

什么是组合式函数

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的lodash或是date-fns。

相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。

鼠标跟踪器示例

如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:

<script setup>
import { ref, onMounted, onUnmounted } from &#39;vue&#39;
const x = ref(0)
const y = ref(0)
function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}
onMounted(() => window.addEventListener(&#39;mousemove&#39;, update))
onUnmounted(() => window.removeEventListener(&#39;mousemove&#39;, update))
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>

但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:

// mouse.js
import { ref, onMounted, onUnmounted } from &#39;vue&#39;
// 按照惯例,组合式函数名以“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(&#39;mousemove&#39;, update))
  onUnmounted(() => window.removeEventListener(&#39;mousemove&#39;, update))
  // 通过返回值暴露所管理的状态
  return { x, y }
}

下面是它在组件中使用的方式:

<script setup>
import { useMouse } from &#39;./mouse.js&#39;
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>

Vue3中的组合式函数编程方法是什么

如你所见,核心逻辑完全一致,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式 API。现在,useMouse()的功能可以在任何组件中轻易复用了。

更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。

举例来说,我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:

// event.js
import { onMounted, onUnmounted } from &#39;vue&#39;
export function useEventListener(target, event, callback) {
  // 如果你想的话,
  // 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

有了它,之前的useMouse()组合式函数可以被简化为:

// mouse.js
import { ref } from &#39;vue&#39;
import { useEventListener } from &#39;./event&#39;
export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  useEventListener(window, &#39;mousemove&#39;, (event) => {
    x.value = event.pageX
    y.value = event.pageY
  })
  return { x, y }
}

TIP

每一个调用useMouse()的组件实例会创建其独有的xy状态拷贝,因此他们不会互相影响。

异步状态示例

useMouse()组合式函数没有接收任何参数,因此让我们再来看一个需要接收一个参数的组合式函数示例。在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。

<script setup>
import { ref } from &#39;vue&#39;
const data = ref(null)
const error = ref(null)
fetch(&#39;...&#39;)
  .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:
    
{{ data }}
Loading...

如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:

// fetch.js
import { ref } from &#39;vue&#39;
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 &#39;./fetch.js&#39;
const { data, error } = useFetch(&#39;...&#39;)
</script>

useFetch()接收一个静态的 URL 字符串作为输入,所以它只执行一次请求,然后就完成了。但如果我们想让它在每次 URL 变化时都重新请求呢?那我们可以让它同时允许接收 ref 作为参数:

// fetch.js
import { ref, isRef, unref, watchEffect } from &#39;vue&#39;
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 &#39;vue&#39;
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 事件监听器或者请求数据),但请注意以下规则:

使用限制

组合式函数在