Composition API

Vue 3 用函数组织组件逻辑的一组 API

它解决了什么问题?

Options API 按 datamethodscomputedwatch 等选项组织代码。组件较大时,同一业务逻辑容易分散在多个选项中;复用逻辑通常还要依赖 Mixins,而 Mixins 存在数据来源不清晰、命名冲突等问题。

Composition API 允许将同一业务关注点的状态、计算属性、方法和生命周期放在一起,并进一步抽取成组合式函数(Composable)。

// useCounter.js
import { computed, ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const double = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  return { count, double, increment }
}
<script setup>
import { useCounter } from './useCounter'

const { count, double, increment } = useCounter(0)
</script>

<template>
  <button @click="increment">
    {{ count }} × 2 = {{ double }}
  </button>
</template>

主要优势

  • 按业务逻辑聚合:相关状态、方法和副作用可以放在一起
  • 逻辑复用清晰:Composable 的输入、输出和依赖都可以显式表达
  • TypeScript 友好:主要使用普通变量和函数,类型推导更自然
  • 更灵活的代码拆分:可按功能拆分模块,也有利于测试
  • 支持 Tree Shaking:未使用的独立 API 更容易被构建工具移除,但不代表业务包体积一定更小

Composition API 并不是 Options API 的替代品。简单组件使用 Options API 仍然合理;复杂组件、逻辑复用较多或 TypeScript 项目通常更适合 Composition API。

setup()

setup() 是 Composition API 的入口,在组件创建过程中、Options API 的大多数选项处理之前执行。

import { ref } from 'vue'

export default {
  props: {
    initialCount: {
      type: Number,
      default: 0
    }
  },
  emits: ['change'],

  setup(props, context) {
    const count = ref(props.initialCount)

    function increment() {
      count.value++
      context.emit('change', count.value)
    }

    // 返回的内容可以在模板中使用
    return { count, increment }
  }
}

props

第一个参数 props 是一个只读的响应式对象。不能直接修改它,也不能随意解构,否则可能丢失响应式关联。

import { toRef, toRefs } from 'vue'

export default {
  props: ['title', 'count'],

  setup(props) {
    // 单个属性
    const title = toRef(props, 'title')

    // 多个属性
    const { count } = toRefs(props)

    return { title, count }
  }
}

如果只读取一次、不需要追踪后续变化,普通解构没有问题。Vue 3.5+ 的 <script setup> 还支持响应式 Props 解构,详见下文。

context

第二个参数是 setup 上下文,可以安全解构:

setup(props, { attrs, slots, emit, expose }) {
  // ...
}
属性作用
attrs未声明为 props 或 emits 的透传属性和事件
slots访问父组件传入的插槽函数
emit触发组件事件
expose控制组件实例向父组件公开的内容

context 本身不是响应式对象。attrs 会保持最新值,但不能通过 watch() 追踪其属性变化;如果需要响应变化,应使用相应的 prop,或者在 onUpdated() 中处理。

返回值

setup() 通常返回一个对象,对象中的属性和方法可以在模板中使用。模板会自动解包顶层 ref,因此模板中不需要写 .value

setup() 也可以直接返回渲染函数,此时不能再同时返回供模板使用的对象。

为什么setup() 中没有this

执行 setup() 时,Options API 中的 datacomputedmethods 等内容还没有初始化完成,因此不能通过组件公开实例的 this 访问它们。Vue 有意让 Composition API 使用变量、函数和参数显式组织依赖。

不要在 setup() 中混用 this

export default {
  data() {
    return { message: 'hello' }
  },

  setup() {
    // 此处不能通过 this.message 访问 data
  }
}

反过来,Options API 可以通过 this 访问 setup() 返回的属性,但新代码不建议依赖这种混合方式。

<script setup>

<script setup> 是单文件组件中使用 Composition API 的推荐语法。代码会被编译为组件的 setup() 内容,顶层变量、函数和导入的组件可以直接在模板中使用。

<script setup>
import { computed, ref } from 'vue'
import UserCard from './UserCard.vue'

const count = ref(0)
const double = computed(() => count.value * 2)
</script>

<template>
  <UserCard />
  <button @click="count++">{{ double }}</button>
</template>

编译宏

definePropsdefineEmitsdefineExpose 等是编译宏,只能在 <script setup> 中使用,不需要从 vue 导入。

<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    title: string
    count?: number
  }>(),
  {
    count: 0
  }
)

const emit = defineEmits<{
  change: [value: number]
}>()

function submit() {
  emit('change', props.count)
}

defineExpose({ submit })
</script>

常用宏包括:

作用
defineProps()声明 props
defineEmits()声明组件事件
withDefaults()为类型形式的 props 提供默认值
defineExpose()指定父组件通过模板 ref 可访问的内容
defineOptions()声明组件名、继承属性等组件选项
defineModel()声明组件的双向绑定,Vue 3.4+ 可用
defineSlots()为插槽提供 TypeScript 类型提示

Props 响应式解构

在 Vue 3.5+ 中,<script setup> 对同一代码块中 defineProps() 解构出的变量进行响应式转换:

<script setup>
const { title, count = 0 } = defineProps({
  title: String,
  count: Number
})

// 父组件更新 prop 后,title 和 count 也会更新
</script>

在 Vue 3.4 及更早版本中,直接解构不会保持响应式,应使用 props.title,或者通过 toRefs(props) 解构。维护旧版本项目时要先确认 Vue 版本。

响应式状态与副作用

Composition API 常用以下几类能力:

  • ref()reactive():声明响应式状态
  • computed():声明派生状态
  • watch()watchEffect():处理响应式副作用
  • onMounted()onUpdated()onUnmounted():注册生命周期逻辑

具体区别可阅读响应式 APIcomputed 与 watchVue 生命周期

生命周期钩子必须在 setup() 同步执行期间注册,才能与当前组件实例关联:

<script setup>
import { onMounted, onUnmounted } from 'vue'

function handleResize() {
  // ...
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
</script>

如何设计 Composable?

Composable 是使用 Composition API 封装和复用有状态逻辑的函数,通常以 use 开头,例如 useMouse()useFetch()

// useEventListener.js
import { onScopeDispose } from 'vue'

export function useEventListener(target, event, handler) {
  target.addEventListener(event, handler)

  onScopeDispose(() => {
    target.removeEventListener(event, handler)
  })
}

onScopeDispose() 可以让清理逻辑跟随调用方的响应式作用域销毁。它比只能依赖组件卸载的写法更容易复用。

设计 Composable 时应注意:

  • 输入参数可以接受普通值、ref 或 getter 时,要明确并统一处理
  • 返回 ref 或包含 ref 的普通对象,方便调用方解构后仍保持响应式
  • 在函数内部完成定时器、监听器和请求等副作用的清理
  • 避免修改调用方未授权的状态,让数据流保持清晰
  • 不要过度拆分;只使用一次且很简单的逻辑可以留在组件内

Composition API 与 Mixins

对比项MixinsComposable
数据来源合并到组件实例,来源不直观通过函数返回值显式获得
命名冲突多个 Mixin 可能冲突调用方可以重命名变量
参数传递通常依赖组件实例使用普通函数参数
TypeScript类型推导较困难类型推导自然
逻辑关系隐式合并显式组合

Composable 复用的是逻辑,不是组件模板。需要同时复用 UI 结构时,应考虑组件或插槽。

常见误区

Composition API 等于 Hooks 吗?

不完全等同。它的组合式函数在使用体验上类似 React Hooks,但 Vue 依靠响应式系统追踪依赖,不依赖固定的调用顺序,也没有 React Hooks 的条件调用限制。

所有状态都应该使用reactive() 吗?

不是。ref() 可用于任意类型,也便于传递和替换;reactive() 适合将相关对象状态组织在一起。选择方式详见响应式 API

setup() 等于created 吗?

只能近似理解为都处于组件创建阶段,不能认为两者完全等价。setup() 的执行时机更早,并承担状态声明、逻辑组合和生命周期注册等职责。

Composable 可以在任意位置调用吗?

纯响应式逻辑可以在普通模块中使用;依赖生命周期、provide/inject 或当前组件实例的 Composable,通常必须在 setup() 同步执行期间调用。

面试总结

Composition API 是 Vue 3 用函数组织组件逻辑的一组 API。它通过 setup()<script setup> 声明响应式状态、派生状态、副作用和生命周期,并可将有状态逻辑抽取为 Composable。

它的核心价值不是“代码更短”,而是让同一业务逻辑聚合、依赖来源明确、逻辑更容易复用和测试。相较 Mixins,Composable 具有显式输入输出、较少命名冲突和更好的 TypeScript 推导。实际开发中应根据组件复杂度选择 Composition API 或 Options API,并注意响应式解构、资源清理和 Vue 版本差异。