Skip to content

组合式函数

什么是组合式函数

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

在构建应用时,我们建厂将一些可复用的公共逻辑抽取为一个函数,这个函数封装了无状态的逻辑,它在接收一些输入后立刻返回期望的输出.

相比之下,有状态逻辑负责管理会随时间而变化的状态.一个煎蛋的例子是跟踪当前鼠标在页面中 的位置

鼠标跟踪器示例

html
<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>

但是如果想要复用这个逻辑呢? 可以把这个逻辑以一个组合式函数的形式提取到外部文件中

javascript
// 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 }
}

组件中使用:

html
<script setup>
  import { useMouse } from './mouse.js'

  const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

这样,核心逻辑完全一样,知识把它移动到另一个外部函数中,并返回需要暴露的状态.和在组件中一样,在组合式函数中可以使用所有的组合式API. 这个例子中useMouse函数的功能就可以在任何组件中复用了

而且,在组合式函数中可以嵌套多个组合式函数,从而将复杂的逻辑拆分成可复用的函数.这也是为什么把这种设计模式成为组合式API

举例来说,我们还可以吧事件监听的逻辑抽取出去:

javascript
// 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函数就可以改造成这样:

javascript
// 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 }
}

异步状态示例

再做异步请求时,我们常需要处理不同的状态:加载中、加载完成、加载失败等.这些状态通常封装在同一个函数中,组合式函数可以很方便地处理异步状态

没有使用组合式函数时:

html
<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>

然而,异步请求比如网络请求使用的很频繁,我们将其抽离成一个组合式函数

javascript
// 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 }
}

这样我们在组件中只需要:

html
<script setup>
  import { useFetch } from './fetch.js'

  const { data, error } = useFetch('...')
</script>

接收响应式状态

组合式函数可以接收一个响应式状态,并返回一个响应式状态.这样,当响应式状态变化时,组合式函数的返回值也会自动更新

javascript
const url = ref('/initial-url')

const { data, error } = useFetch(url)

// 这将会重新触发 fetch
url.value = '/new-url'

或者接收一个getter函数

javascript
// 当 props.id 改变时重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)

这个特性是不是想到了watchEffect?同样,我们也可以使用watchEffecttoValue来重构

javascript
// 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.它的设计目的是将refgetter规范化为值.如果参数是ref,则返回其内部值;如果参数是getter函数,则调用函数并返回其值. 否则它会原样返回参数.工作方式类似于unref,但对函数又特殊处理

注意,toValue(url)的调用是在watchEffect内部进行的,这样当url变化时,fetchData函数会重新执行,从而重新发起请求

这样,我们改造后哦的useFetch()能接收静态字符串/ref/getter,更加灵活,watchEffect会立即运行,并且会跟踪toValue(url)期间访问的任何依赖, 当url变化时,fetchData会重新执行

约定和最佳实践

命名

组合是函数约定用驼峰命名法,并使用use作为前缀

输入参数

组合式函数可以接收ref``getter或静态字符串作为参数,需要进行处理

javascript
import { toValue } from 'vue'

function useFeature(maybeRefOrGetter) {
  // 如果 maybeRefOrGetter 是一个 ref 或 getter,
  // 将返回它的规范化值。
  // 否则原样返回。
  const value = toValue(maybeRefOrGetter)
}

返回值

在之前的示例代码中,我们在组合式函数中一直使用ref()而不是reactive().约定组合式函数始终返回一个包含多个ref的普通的非响应式对象,这样对象在组件中被结构为ref时,仍能保持响应性

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

如果从组合式函数中返回一个响应式对象,会导致对象在结构过程中丢失与组合是函数内部状态的响应性连接,而ref则不会

如果想以对象的形式来使用组合式函数中返回的状态,可以讲返回的状态用reactive()包裹起来

javascript
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
html
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}

副作用

在组合式函数中添加副作用需要注意一下规则:

  • 如果应用了服务端渲染(SSR),需确保在组件挂载后才调用的生命周期钩子中执行DOM相关的副作用,例如onMounted.这些钩子仅会在浏览器中被调用
  • 确保在onUnmounted中清理副作用,防止内存泄漏.比如注册了时间监听器,就需要在onUnmounted中移除监听器

使用限制

组合式函数只能在<script setup>标签或setup()钩子中同步调用.这些限制很重要,因为这些事Vue用于确定当前活跃的组件实例的上下文. 访问活跃的组件实例很有必要,这样才能:

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

通过抽取组合式函数改善代码结构

抽取组合式函数不仅可以复用逻辑,也可以简化代码结构.特别是组件的复杂度增高时,使用组合式API可以基于逻辑问题讲组件的代码拆分为更小的函数

html
<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()函数是组件的入口,且其返回值的绑定必须在setup()中返回,一遍暴露给this及其模板

javascript
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)
  },
  // ...其他选项
}

与其他模式的比较

Vue2 中的 Mixin

定义

Mixin 是一个包含组件选项的普通 JavaScript 对象:

javascript
// myMixin.js
export const myMixin = {
  data() {
    return {
      mixinData: '来自Mixin的数据',
    }
  },
  methods: {
    mixinMethod() {
      console.log('Mixin方法被调用')
    },
  },
}
使用
javascript
import { myMixin } from './myMixin.js'

export default {
  mixins: [myMixin],
  created() {
    console.log(this.mixinData) // 可以访问Mixin的数据
    this.mixinMethod() // 可以调用Mixin的方法
  },
}

特性

合并策略

当组件和Mixin包含相同选项时,Vue有特定的合并策略:

  • 对于datamethodscomputed,Vue会合并它们,组件的选项会覆盖Mixin的选项。
  • 对于生命周期钩子,它们会被合并成一个数组,组件和Mixin的钩子都会被调用。Mixin的先执行
全局 Mixin

可通过Vue.mixin()注册全局Mixin(慎用)

典型问题

命名冲突
// mixinA
{
  data() { return { count: 0 } },
  methods: { increment() { this.count++ } }
}

// mixinB
{
  data() { return { count: 100 } },
  methods: { increment() { this.count += 10 } }
}

// 组件
{
  mixins: [mixinA, mixinB],
  methods: {
    print() {
      this.increment() // 哪个increment会被调用?
      console.log(this.count) // 输出什么?
    }
  }
}
隐式依赖

Mixin内部可能依赖特定上下文或其它Mixin,但组件可能没有引用

难以追踪来源

当多个Mixin混入时,很难确定某个属性/方法具体来自哪个Mixin。

组合式函数的优点

  • 更星期的数据来源
  • 使用组合式API可以在结构变量时对变量进行重命名避免冲突

和无渲染组件的对比

通过插槽定义的无渲染组件可以实现和组合式函数相同的功能,但组合式函数不会产生额外的组件实例开销.当在整个应用中使用时,无渲染组件产生的额外示例开销会带来无法忽视的性能开销