三十二、组合式函数


组合式函数,本质上也就是代码复用的一种方式。

  • 组件:对结构、样式、逻辑进行复用
  • 组合式函数:侧重于对 有状态 的逻辑进行复用

快速上手

实现一个鼠标坐标值的追踪器。

<template>
  <div>当前鼠标位置: {{ x }}, {{ y }}</div>
</template>

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

<style scoped></style>

多个组件中复用这个相同的逻辑,该怎么办?

答:使用组合式函数。将包含了状态的相关逻辑,一起提取到一个单独的函数中,该函数就是组合式函数。

相关细节

1. 组合式函数本身还可以相互嵌套

2. 和Vue2时期mixin区别

解决了 Vue2 时期 mixin 的一些问题。

  1. 不清晰的数据来源:当使用多个 minxin 的时候,实例上的数据属性来自于哪一个 mixin 不太好分辨。
  2. 命名空间冲突:如果多个 mixin 来自于不同的作者,可能会注册相同的属性名,造成命名冲突

mixin

const mixinA = {
  methods: {
    fetchData() {
      // fetch data logic for mixin A
      console.log('Fetching data from mixin A');
    }
  }
};

const mixinB = {
  methods: {
    fetchData() {
      // fetch data logic for mixin B
      console.log('Fetching data from mixin B');
    }
  }
};

new Vue({
  mixins: [mixinA, mixinB],
  template: `
    <div>
      <button @click="fetchData">Fetch Data</button>
    </div>
  `
});

组合式函数:

// useMixinA.js
import { ref } from 'vue';

export function useMixinA() {
  function fetchData() {
    // fetch data logic for mixin A
    console.log('Fetching data from mixin A');
  }

  return { fetchData };
}

// useMixinB.js
import { ref } from 'vue';

export function useMixinB() {
  function fetchData() {
    // fetch data logic for mixin B
    console.log('Fetching data from mixin B');
  }

  return { fetchData };
}

组件使用上面的组合式函数:

import { defineComponent } from 'vue';
import { useMixinA } from './useMixinA';
import { useMixinB } from './useMixinB';

export default defineComponent({
  setup() {
    // 这里必须要给别名
    const { fetchData: fetchDataA } = useMixinA();
    const { fetchData: fetchDataB } = useMixinB();

    fetchDataA();
    fetchDataB();

    return { fetchDataA, fetchDataB };
  },
  template: `
    <div>
      <button @click="fetchDataA">Fetch Data A</button>
      <button @click="fetchDataB">Fetch Data B</button>
    </div>
  `
});
  1. 隐式的跨mixin交流

mixin

export const mixinA = {
  data() {
    return {
      sharedValue: 'some value'
    };
  }
};
export const minxinB = {
  computed: {
    dValue(){
      // 和 mixinA 具有隐式的交流
      // 因为最终 mixin 的内容会被合并到组件实例上面,因此在 mixinB 里面可以直接访问 mixinA 的数据
      return this.sharedValue + 'xxxx';
    }
  }
}

组合式函数:交流就是显式的

import { ref } from 'vue';

export function useMixinA() {
  const sharedValue = ref('some value');
  return { sharedValue };
}
import { computed } from 'vue';

export function useMixinB(sharedValue) {
  const derivedValue = computed(() => sharedValue.value + ' extended');
  return { derivedValue };
}
<template>
  <div>
    {{ derivedValue }}
  </div>
</template>

<script>
import { defineComponent } from 'vue';
import { useMixinA } from './useMixinA';
import { useMixinB } from './useMixinB';

export default defineComponent({
  setup() {
    const { sharedValue } = useMixinA();
    
    // 两个组合式函数的交流是显式的
    const { derivedValue } = useMixinB(sharedValue);

    return { derivedValue };
  }
});
</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>

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

如何复用这段逻辑?仍然是提取成一个组合式函数。

如下:

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

现在重构上面的组件:

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

<script setup>
import {useFetch} from './hooks/useFetch';
const {data, error} = useFetch('xxxx')
</script>

这里为了更加灵活,我们想要传递一个响应式数据:

const url = ref('first-url');
// 请求数据
const {data, error} = useFetch(url);
// 修改 url 的值后重新请求数据
url.value = 'new-url';

此时我们就需要重构上面的组合式函数:

import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    // 每次执行 fetchData 的时候,重制 data 和 error 的值
    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 }
}

约定和最佳实践

1. 命名:组合式函数约定用驼峰命名法命名,并以“use”作为开头。例如前面的 useMouse、useEvent.

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

3. 返回值

组合式函数中推荐返回一个普通对象,该对象的每一项是 ref 数据,这样可以保证在解构的时候仍然能够保持其响应式的特性:

// 组合式函数
export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  // ...

  return { x, y }
}
import { useMouse } from './hooks/useMouse'
// 可以解构
const { x, y } = useMouse()

如果希望以对象属性的形式来使用组合式函数中返回的状态,可以将返回的对象用 reactive 再包装一次即可:

import { useMouse } from './hooks/useMouse'
const mouse = reactive(useMouse())

4. 副作用

在组合式函数中可以执行副作用,例如添加 DOM 事件监听器或者请求数据。但是请确保在 onUnmounted 里面清理副作用。

例如在一个组合式函数设置了一个事件监听器,那么就需要在 onUnmounted 的时候移除这个事件监听器。

export function useMouse() {
  // ...

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // ...
}

也可以像前面 useEvent 一样,专门定义一个组合式函数来处理副作用:

import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  // 专门处理副作用的组合式函数
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

5. 使用限制

  1. 只能在 setup标签 或 setup( ) 钩子中调用:确保在组件实例被创建时,所有的组合式函数都被正确初始化。特别如果你使用的是选项式 API,那么需要在 setup 方法中调用组合式函数,并且返回,这样才能暴露给 this 及其模板使用
import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
  setup() {
    // 因为组合式函数会返回一些状态
    // 为了后面通过 this 能够正确访问到这些数据状态
    // 必须在 setup 的时候调用组合式函数
    const { x, y } = useMouse()
    const { data, error } = useFetch('...')
    return { x, y, data, error }
  },
  mounted() {
    // setup() 暴露的属性可以在通过 `this` 访问到
    console.log(this.x)
  }
  // ...其他选项
}
  1. 只能被同步调用:组合式函数需要同步调用,以确保在组件实例的初始化过程中,所有相关的状态和副作用都能被正确地设置和处理。如果组合式函数被异步调用,可能会导致在组件实例还未完全初始化时,尝试访问未定义的实例数据,从而引发错误。
  2. 可以在像 onMounted 生命周期钩子中调用:在某些情况下,可以在如 onMounted 生命周期钩子中调用组合式函数。这些生命周期钩子也是同步执行的,并且在组件实例已经被初始化后调用,因此可以安全地使用组合式函数。

6. 执行顺序

在 Vue 3 中,组合式函数(Composition Functions)和组件(Component)中都会使用生命周期钩子。它们的调用顺序可以归纳为以下几点:

  1. 组合式函数中的生命周期钩子在组件自身生命周期钩子之前被调用。
  2. 同一个生命周期钩子(如<font style="color:rgb(51, 51, 51);">onMounted</font>)在所有组合式函数中按照它们被调用的顺序被执行。
  3. 组合式函数中的钩子执行完毕后,再执行组件自身的钩子。

具体来看一个例子,以标明不同生命周期钩子的顺序:

import { onMounted, onBeforeUnmount, defineComponent } from 'vue';

function useExample() {
  onMounted(() => {
    console.log('useExample onMounted');
  });

  onBeforeUnmount(() => {
    console.log('useExample onBeforeUnmount');
  });
}

export default defineComponent({
  setup() {
    useExample();

    onMounted(() => {
      console.log('Component onMounted');
    });

    onBeforeUnmount(() => {
      console.log('Component onBeforeUnmount');
    });
  },
});

在这个例子里,当组件被挂载和销毁时,控制台的输出顺序将会是:

useExample onMounted
Component onMounted

当组件被销毁时,控制台的输出顺序将会是:

useExample onBeforeUnmount
Component onBeforeUnmount

这种执行顺序是因为组合式函数的生命周期钩子先被注册并执行,然后才是组件自身的钩子。这有助于确保组合式函数的逻辑能够先于组件自身的逻辑运行。


文章作者: 吴俊杰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 吴俊杰 !
  目录