Hooks在Vue文档中叫组合式函数。什么是组合式函数,组合式函数是一个利用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) {
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()组合式函数没有接收任何参数,这里举一个需要接收一个参数的组合式函数示例。在做异步数据请求时,常常需要处理不同的状态:加载中、加载成功和加载失败。

<script setup>
import { ref } from 'vue'

const date = 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,就需要将响应式 状态传入组合式函数,并让它基于传入的状态来创建执行操作的监听器。

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 = () => {
//请求之前重置数据
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) {
const value = toValue(maybeRefOrGetter)
}

如果组合式函数在输入参数是ref或getter的情况下创建了响应式effect,为了让它能够被正确追踪,请确保要么使用watch()显式地监视ref或getter,要么在watchEffect()中调用 toValue()。

返回值

前面一直在组合式函数中使用ref()而不是reactive()。推荐的约定时组合式函数始终返回一个包含多个ref的普通的非响应式对象,这样该对象在组件中被解构为ref之后仍可以保持响应性:

// x和y是两个ref
const { x, y } = useMouse()

从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref则可以维持这一响应性连接。

副作用

在组合式函数中可以执行副作用,但是需要注意以下的规则:

  1. 如果应用用到了SSR,请确保在组件挂载后才调用的生命周期钩子中执行DOM相关的副作用。
  2. 确保在onUnmounted()时清理副作用。

使用限制

组合式函数只能在<script setup>setup()钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,也可以在像onMounted()这样的生命周期钩子中调用。

这些限制很重要,因为这些时Vue用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:

  1. 将生命周期钩子注册到该组件实例上
  2. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄露