Composition API 核心
Vue 3 推出的 Composition API 是其最重要的特性之一,它从根本上重构了组织组件逻辑的方式。与 Vue 2 的选项式 API(Options API)将代码按功能类型(data、methods、computed等)分散在不同选项中不同,Composition API 允许开发者将同一功能的逻辑(如数据、方法、计算属性、侦听器等)聚合在一起,形成一个逻辑单元。这种范式带来了无与伦比的灵活性,使得逻辑的复用、提取和测试变得异常简单,极大地提升了大型项目的可维护性。<script setup> 语法糖是 Composition API 在单文件组件中的最佳伴侣,它让代码更简洁、类型推导更友好。
ref vs reactive
在 Composition API 中,我们需要使用 ref 和 reactive 来创建响应式数据。理解它们的核心区别、适用场景以及底层原理,是编写高效 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)中返回多个状态时,习惯上使用
ref或toRefs以确保调用者解构后仍保持响应性。
生命周期
Composition API 中的生命周期钩子是在 setup 函数中同步调用的函数,它们将组件的生命周期钩子注入到当前组件实例中。这使得逻辑的关注点分离更加极致,你可以将某个功能的所有生命周期代码(例如,在挂载时获取数据,在卸载时清除定时器)封装在同一个函数或代码块内,而不是分散在选项式 API 的不同生命周期钩子里。
常用生命周期钩子
setup 本身相当于 beforeCreate 和 created 钩子,因此不再需要这两个钩子。其他钩子需要从 vue 中导入并使用。
onMounted: 组件挂载到 DOM 后调用。这是发起网络请求、操作 DOM(需要访问实际 DOM 元素时)、启动第三方库(如地图、图表)的理想时机。onUpdated: 响应式数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。注意:如果需要在此钩子中访问更新后的 DOM,请谨慎使用,因为其调用时机在子组件更新之后。通常优先使用watch或watchEffect。onUnmounted: 组件实例被卸载之后调用。这里是进行清理操作的最后机会,例如清除定时器、取消事件监听、断开 WebSocket 连接等。onErrorCaptured: 当捕获到一个来自后代组件的错误时被调用。可用于实现全局错误边界。
其他进阶钩子
onBeforeMount/onBeforeUpdate/onBeforeUnmount: 对应选项式 API 的beforeMount、beforeUpdate、beforeUnmount,在对应阶段开始前调用。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 中将 data、created、methods、beforeDestroy 分开定义要直观得多,尤其是在组件功能复杂时。这种内聚性正是 Composition API 提升代码可维护性的关键。
评论
暂无评论,来写第一条吧
