Vue 3 Composition API 实战指南

2024年12月28日 测试分类 12 分钟阅读 32 次阅读

Composition API 核心

Vue 3 推出的 Composition API 是其最重要的特性之一,它从根本上重构了组织组件逻辑的方式。与 Vue 2 的选项式 API(Options API)将代码按功能类型(datamethodscomputed等)分散在不同选项中不同,Composition API 允许开发者将同一功能的逻辑(如数据、方法、计算属性、侦听器等)聚合在一起,形成一个逻辑单元。这种范式带来了无与伦比的灵活性,使得逻辑的复用、提取和测试变得异常简单,极大地提升了大型项目的可维护性。<script setup> 语法糖是 Composition API 在单文件组件中的最佳伴侣,它让代码更简洁、类型推导更友好。

ref vs reactive

在 Composition API 中,我们需要使用 refreactive 来创建响应式数据。理解它们的核心区别、适用场景以及底层原理,是编写高效 Vue 3 代码的基础。

`ref`:响应式引用

ref 函数用于创建一个包含响应式数据的“引用对象”。它的核心职责是包装任何类型的值,使其成为响应式。对于原始值(如数字、字符串、布尔值),它几乎是唯一的选择。ref 通过其 .value 属性来访问和修改内部值。在模板中,Vue 会自动“解包”,因此不需要写 .value

原理深入: ref 创建的对象其实是一个 { value: ... } 的包装器。当值为对象时,其内部会递归地调用 reactive 将其转换为深层响应式对象。它通过 Object.defineProperty (Vue 2) 或更现代的 Proxy (Vue 3) 来拦截 .value 的 get/set 操作,从而触发依赖收集和更新通知。

应用场景与最佳实践:

  • 适用于原始值: 当你需要一个响应式的数字、字符串或布尔值时,ref 是标准且必要的选择。
  • 适用于需要替换整个引用的场景: 当你有一个对象,但后续可能会将其整个替换为一个新对象(例如,从 API 获取新数据并直接赋值),使用 ref 更为直接,因为你只需更新 .value 即可。
  • toRefs 配合解构: 当从 reactive 对象中解构属性时,会丢失响应性。此时可以使用 toRefs 将每个属性转换为 ref,从而安全地解构。
<script setup>
import { ref } from 'vue'

// 创建一个 ref 响应式变量
const count = ref(0) // 包装原始值
const message = ref('Hello Vue 3!')

// 在 JavaScript 中,必须通过 .value 来访问和修改
console.log(count.value) // 0
count.value++
message.value = 'New Message'

// 函数中操作 ref 也很清晰
function increment() {
  count.value++
}
</script>
<template>
  <!-- 在模板中,ref 会被自动解包,无需写 .value -->
  <p>{{ count }}</p>
  <p>{{ message }}</p>
  <button @click="increment">Increment</button>
</template>

`reactive`:响应式对象

reactive 函数接收一个普通 JavaScript 对象(或数组),并返回该对象的深层响应式代理。对这个代理对象的任何属性进行读写操作,Vue 都能检测到,并触发相应的视图更新。你不需要使用 .value,操作方式与普通对象无异。

原理深入: reactive 的核心是利用 ES6 的 Proxy API 创建一个对象的代理。这个代理会拦截对象的各种基本操作(如属性访问、赋值、删除等)。当你在 setup 函数中访问 reactive 对象的属性时,Vue 的响应式系统会记录依赖;当你修改属性时,它会通知所有依赖该属性的组件或计算属性进行更新。

应用场景与最佳实践:

  • 适用于复杂对象或数组: 当你管理表单状态、用户信息、列表数据等结构化的数据时,reactive 是更自然、更符合直觉的选择。
  • 注意解构丢失响应性: 这是新手最容易犯的错误。直接从 reactive 对象中解构属性会得到一个普通变量,它不再是响应式的。解决方法有二:一是直接通过对象访问(state.name),二是使用 toRefs 进行解构。
  • 谨慎使用在可替换的顶层状态: reactive 对象本身是常量,你不能将其整个替换为一个新对象(例如,state = newObj 会断开响应式链接)。如果你需要这种灵活性,应该使用 ref 包装一个对象(const state = ref({...})),然后通过 state.value = newObj 来更新。
<script setup>
import { reactive } from 'vue'

// 创建一个深层响应式的对象
const state = reactive({
  name: 'Vue',
  version: 3,
  features: ['Composition API', 'Teleport', 'Fragments']
})

// 直接修改属性,响应式生效
state.name = 'Vue 3'
state.features.push('Suspense')

// 错误示例:解构会丢失响应性
const { name, version } = state // name 和 version 不再是响应式变量!
// 正确做法:直接使用 state.name, state.version,或使用 toRefs
</script>
<template>
  <!-- 直接访问代理对象的属性 -->
  <p>{{ state.name }} - v{{ state.version }}</p>
  <ul>
    <li v-for="feature in state.features" :key="feature">{{ feature }}</li>
  </ul>
</template>

`ref` 与 `reactive` 对比总结与选择指南

特性 ref reactive
接受参数 任意值(原始值、对象、数组) 对象或数组
访问方式 需要通过 .value 访问 直接访问属性
替换整个值 支持 (myRef.value = newValue) 不支持,会断开响应式
模板解构 自动解包,直接使用 直接使用属性
类型推导 Ref<T> 对原始类型进行 Reactive<T> 推导
最佳场景 原始值;需要替换整体引用的复杂数据 管理多个相关字段的复杂对象

黄金法则:

  • 当数据是原始类型,或者你预期未来会整体替换这个数据时,用 ref
  • 当数据是一个结构稳定的对象,并且你主要操作其内部属性时,用 reactive
  • 在组合式函数(Composables)中返回多个状态时,习惯上使用 reftoRefs 以确保调用者解构后仍保持响应性。

生命周期

Composition API 中的生命周期钩子是在 setup 函数中同步调用的函数,它们将组件的生命周期钩子注入到当前组件实例中。这使得逻辑的关注点分离更加极致,你可以将某个功能的所有生命周期代码(例如,在挂载时获取数据,在卸载时清除定时器)封装在同一个函数或代码块内,而不是分散在选项式 API 的不同生命周期钩子里。

常用生命周期钩子

setup 本身相当于 beforeCreatecreated 钩子,因此不再需要这两个钩子。其他钩子需要从 vue 中导入并使用。

  • onMounted: 组件挂载到 DOM 后调用。这是发起网络请求、操作 DOM(需要访问实际 DOM 元素时)、启动第三方库(如地图、图表)的理想时机。
  • onUpdated: 响应式数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。注意:如果需要在此钩子中访问更新后的 DOM,请谨慎使用,因为其调用时机在子组件更新之后。通常优先使用 watchwatchEffect
  • onUnmounted: 组件实例被卸载之后调用。这里是进行清理操作的最后机会,例如清除定时器、取消事件监听、断开 WebSocket 连接等。
  • onErrorCaptured: 当捕获到一个来自后代组件的错误时被调用。可用于实现全局错误边界。

其他进阶钩子

  • onBeforeMount / onBeforeUpdate / onBeforeUnmount: 对应选项式 API 的 beforeMountbeforeUpdatebeforeUnmount,在对应阶段开始前调用。
  • onActivated / onDeactivated: 配合 <KeepAlive> 缓存的组件使用。
  • onServerPrefetch (SSR only): 在服务器端渲染期间,用于执行异步数据预取。

原理与最佳实践:

  • 逻辑内聚: Composition API 的魅力在于逻辑复用。因此,最好的实践是将某个功能相关的状态(ref/reactive)、计算属性(computed)、方法以及它所依赖的生命周期钩子,封装到一个独立的“组合式函数”(Composable)中。例如,一个 useMousePosition 组合式函数可以在 onMounted 中添加事件监听,在 onUnmounted 中移除。
  • 异步操作: onMounted 及其他钩子本身不是异步的。如果需要在挂载后进行异步操作(如 fetch),可以直接在钩子内部使用 async/await,但要注意这不会阻塞组件的渲染。
  • 访问模板引用: 必须在组件挂载后(即在 onMounted 钩子或其之后)才能访问通过 ref 属性设置的 DOM 元素。
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const list = ref([])
const error = ref(null)
let timer = null

// 模拟数据获取
async function fetchData() {
  try {
    // 实际项目中替换为 fetch 或 axios 调用
    const response = await new Promise(resolve => 
      setTimeout(() => resolve({ data: [1, 2, 3] }), 1000)
    )
    list.value = response.data
  } catch (e) {
    error.value = e.message
  }
}

// 组件挂载后执行
onMounted(() => {
  console.log('组件已挂载,开始获取数据...')
  fetchData()
  
  // 启动一个定时器,注意在组件卸载时清除
  timer = setInterval(() => {
    console.log('Timer ticking...')
  }, 1000)
})

// 组件卸载前执行清理
onUnmounted(() => {
  console.log('组件即将卸载,清理定时器。')
  if (timer) {
    clearInterval(timer)
    timer = null
  }
})
</script>
<template>
  <div v-if="error">{{ error }}</div>
  <ul v-else>
    <li v-for="item in list" :key="item">{{ item }}</li>
  </ul>
</template>

在上面的示例中,与“数据获取”和“定时器”功能相关的所有逻辑(状态、异步操作、生命周期钩子)都集中在 setup 的顶部,形成清晰的逻辑块。这比选项式 API 中将 datacreatedmethodsbeforeDestroy 分开定义要直观得多,尤其是在组件功能复杂时。这种内聚性正是 Composition API 提升代码可维护性的关键。

最后更新:2026年7月5日CC BY-NC-SA 4.0

评论

暂无评论,来写第一条吧

© 2026 My Blog. Built with Nuxt.js + FastAPI.