Skip to content

组合式 API(Composition API)

Vue 3 中的组合式 API 是一种新的方式,用来组织和复用 Vue 组件中的逻辑。与 Vue 2 中的 Options API(通过 data, methods, computed, watch 等选项来定义组件)不同,组合式 API 使用函数来声明组件的逻辑和状态,使得代码更具模块化和可维护性。

核心理念

组合式 API 主要通过函数的方式,将组件的逻辑组织在一起。其主要目标是:

  • 提高代码的可读性和可维护性: 特别是对于复杂组件,通过将逻辑分割成更小的函数,可以使代码更易于理解和维护。
  • 增强逻辑复用性: 可以将逻辑提取到独立的函数或文件中,以便在多个组件间共享。

主要组成部分

1. setup()函数

setup() 函数是在组件中使用组合式 API 的入口,它是在组件实例创建之前调用的,用于初始化组件的逻辑。通常只在以下情况下使用:

  • 需要在非单文件组件中使用组合式 API 时。
  • 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。

注意

对于结合单文件组件 (SFC) 使用的组合式 API,推荐通过 <script setup> 以获得更加简洁及符合人体工程学的语法。 <script setup>是在单文件组件中使用组合式 API 的编译时语法糖。

js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    const increment = () => {
      count.value++
    }

    return {
      count,
      increment,
    }
  },
}

setup 函数的第一个参数是组件的 props,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。

setup 函数的第二个参数是一个Setup 上下文对象 context,它包含了以下属性:

  • attrs:透传 Attributes(非响应式的对象,等价于 $attrs)
  • slots:插槽(非响应式的对象,等价于 $slots)
  • emit:触发事件(函数,等价于 $emit)
  • expose:暴露公共属性(函数)

setup 函数返回的对象会暴露给模板和组件实例。

2. 响应式 API

ref()

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

js
const count = ref(0)
count.value++ // 修改值需要通过 .value 属性

reactive()

用于创建一个响应式对象。

js
const state = reactive({
  count: 0,
})
state.count++ // 直接修改对象属性

computed()

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

js
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误
js
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  },
})

plusOne.value = 1
console.log(count.value) // 0
js
/**
 * 计算属性的 onTrack 和 onTrigger 选项仅会在开发模式下工作。
 * onTrack 将在响应属性或引用作为依赖项被跟踪时被调用。
 * onTrigger 将在侦听器回调被依赖项的变更触发时被调用。
 */
const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // 当 count.value 被追踪为依赖时触发
    debugger
  },
  onTrigger(e) {
    // 当 count.value 被更改时触发
    debugger
  },
})

// 访问 plusOne,会触发 onTrack
console.log(plusOne.value)

// 更改 count.value,应该会触发 onTrigger
count.value++

watch()

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

第一个参数是侦听器的。这个来源可以是以下几种:

  • 一个函数,返回一个值
  • 一个 ref
  • 一个响应式对象
  • ...或是由以上类型的值组成的数组

第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

第三个可选的参数是一个对象,支持以下这些选项:

  • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
  • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。
  • flush:调整回调函数的刷新时机。
  • onTrack / onTrigger:调试侦听器的依赖。
  • once:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。
js
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)
js
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})
js
/**
 * 当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值
 */
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})
js
watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  },
})

当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果想让回调在深层级变更时也能触发,需要使用 { deep: true } 强制侦听器进入深层级模式。在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。

js
const state = reactive({ count: 0 })
watch(
  () => state,
  (newValue, oldValue) => {
    // newValue === oldValue
  },
  { deep: true }
)

当直接侦听一个响应式对象时,侦听器会自动启用深层模式:

js
const state = reactive({ count: 0 })
watch(state, () => {
  /* 深层级变更状态所触发的回调 */
})

3. 生命周期钩子

  • onMounted(): 注册一个回调函数,在组件挂载完成后执行
  • onUpdated(): 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
  • onUnmounted(): 注册一个回调函数,在组件实例被卸载之后调用。
  • onBeforeMount(): 注册一个钩子,在组件被挂载之前被调用。
  • onBeforeUpdate(): 注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。
  • onBeforeUnmount(): 注册一个钩子,在组件实例被卸载之前调用。
  • 。。。
vue
<script setup>
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
  console.log('Component mounted')
})

onUnmounted(() => {
  console.log('Component unmounted')
})
</script>

4. 依赖注入

provide()

提供一个值,可以被后代组件注入。

provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。

vue
<script setup>
import { ref, provide } from 'vue'
import { countSymbol } from './injectionSymbols'

// 提供静态值
provide('path', '/project/')

// 提供响应式的值
const count = ref(0)
provide('count', count)

// 提供时将 Symbol 作为 key
provide(countSymbol, count)
</script>

inject()

注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。

第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

第二个参数是可选的,即在没有匹配到 key 时使用的默认值。

第二个参数也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。在这种情况下,必须将 true 作为第三个参数传入,表明这个函数将作为工厂函数使用,而非值本身。

vue
<script setup>
import { inject } from 'vue'
import { countSymbol } from './injectionSymbols'

// 注入不含默认值的静态值
const path = inject('path')

// 注入响应式的值
const count = inject('count')

// 通过 Symbol 类型的 key 注入
const count2 = inject(countSymbol)

// 注入一个值,若为空则使用提供的默认值
const bar = inject('path', '/default-path')

// 注入一个值,若为空则使用提供的函数类型的默认值
const fn = inject('function', () => {})

// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('factory', () => new ExpensiveObject(), true)
</script>

5. 自定义 Hook

自定义 Hook(也称为组合函数)允许你将逻辑提取到独立的函数中,以便在多个组件中复用。

js
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)

  const increment = () => {
    count.value++
  }

  return { count, increment }
}
vue
<script setup>
import { useCounter } from './useCounter'
const { count, increment } = useCounter()
</script>

<template>
  count: {{ count }}
  <button @click="increment">increment</button>
</template>

count: 0

increment

与 Options API 的对比

  • 逻辑组织: Composition API 将组件的逻辑更清晰地组织成函数,而不是分散在多个选项(如 data、methods、computed 等)中。
  • 逻辑复用: 可以更容易地提取和复用逻辑。
  • 类型支持: 对于 TypeScript 用户,Composition API 提供了更好的类型推导和支持。