Composition API与响应式系统
为什么需要 Composition API
概念
Composition API 的诞生是为了解决 Options API 在大型、复杂组件开发中遇到的痛点。
在 Options API 中,一个功能的实现逻辑(数据、方法、计算属性等)被迫分散在 data、methods、computed 等不同选项中。当组件变得复杂时,相关联的代码被拆分,而不相关的代码却聚合在一起。这导致开发者在理解和维护某个特定功能时,需要在单个文件中反复跳转,极大地降低了开发效率和代码的可读性。
Composition API 提出了一个核心思想:根据逻辑功能来组织代码,而不是根据选项类型。
代码示例
❌ Options API 的痛点:逻辑分散
在下面的示例中,用户、文章和搜索这三个功能的逻辑分别散落在 data、computed 和 methods 中。
// 示例:一个包含用户、文章、搜索三个功能的复杂组件
export default {
// 1. 数据定义区
data() {
return {
// 用户相关数据
user: null,
userLoading: false,
// 文章相关数据
articles: [],
articlesLoading: false,
// 搜索相关数据
searchQuery: '',
}
},
// 2. 计算属性区
computed: {
// 用户相关计算属性
userFullName() {
/* ... */
},
// 文章相关计算属性
publishedArticles() {
/* ... */
},
},
// 3. 方法区
methods: {
// 用户相关方法
async fetchUser() { /* ... */ },
updateUser() { /* ... */ },
// 文章相关方法
async fetchArticles() { /* ... */ },
deleteArticle() { /* ... */ },
// 搜索相关方法
performSearch() { /* ... */ },
}
// 😩 当需要修改“用户”功能时,你需要在 data, computed, methods 之间来回跳转。
}✅ Composition API 的优势:逻辑聚合
使用 Composition API,我们可以将同一个功能的代码封装在独立的函数(称为 "Composable")中,然后在 setup 函数中组合使用。
import { ref, computed } from 'vue'
// ------------------------------------
// 功能一:用户管理 (User Logic)
// ------------------------------------
function useUser() {
// 数据
const user = ref(null)
const userLoading = ref(false)
// 计算属性
const userFullName = computed(() => /* ... */)
// 方法
async function fetchUser() { /* ... */ }
function updateUser(data) { /* ... */ }
// 将所有与用户相关的 API 一起返回
return { user, userLoading, userFullName, fetchUser, updateUser }
}
// ------------------------------------
// 功能二:文章管理 (Articles Logic)
// ------------------------------------
function useArticles() {
const articles = ref([])
const articlesLoading = ref(false)
const publishedArticles = computed(() => /* ... */)
async function fetchArticles() { /* ... */ }
function deleteArticle(id) { /* ... */ }
return { articles, articlesLoading, publishedArticles, fetchArticles, deleteArticle }
}
// ------------------------------------
// 在组件中组合使用
// ------------------------------------
export default {
setup() {
// 引入用户功能
const { user, userFullName, fetchUser } = useUser()
// 引入文章功能
const { articles, publishedArticles, fetchArticles } = useArticles()
// 😊 相关逻辑聚合,不相关逻辑分离,代码组织清晰!
// 将需要暴露给模板的变量和方法返回
return {
user,
userFullName,
articles,
publishedArticles,
fetchUser,
fetchArticles
}
}
}深入解析
Composition API 带来了思维模式的转变:
更优的逻辑复用:通过组合函数 (Composables) 来复用逻辑,取代了mixins。Composables 是普通的 JavaScript 函数,解决了 mixins 的命名冲突、数据来源不清晰和隐式依赖等问题。更好的类型推断:代码基于普通的函数和变量,TypeScript 可以完美地进行类型推断,解决了 Options API 中this的类型推断难题。更灵活的代码组织:你可以将复杂逻辑拆分到多个独立的文件中,让大型项目维护起来更加轻松。更小的打包体积:Composition API 的函数是按需导入的,例如ref,computed等。没有用到的代码可以被构建工具进行 "Tree-shaking",从而减小最终的打包体积。
面试建议
问:"为什么 Vue 3 要引入 Composition API?它解决了什么问题?"
回答要点:
先肯定 Options API:"Options API 在中小型项目中非常直观易懂,这是 Vue 易学性的重要体现。"再指出其痛点:"然而,在开发大型复杂组件时,Options API 会导致逻辑关注点分散,相关代码被拆分到 data, methods 等不同选项中,难以维护。同时,依赖 mixins 的逻辑复用方案存在命名冲突和数据来源不清晰等问题,且对 TypeScript 的类型推断不够友好。"最后引出解决方案:"Composition API 正是为了解决这些问题而生的。它允许我们通过组合函数的方式,将相关逻辑聚合在一起,实现了更清晰、更可维护、类型更安全的代码组织和逻辑复用模式。"补充说明:"需要强调的是,Composition API 是对 Options API 的补充而非替代,开发者可以根据项目复杂度和团队习惯灵活选择。"
核心 API 详解
ref() - 创建基础类型的响应式引用
ref 用于将一个基础类型的值(如 string, number, boolean)包装成一个响应式的对象。
基本用法
// MyComponent.vue
import { ref } from 'vue'
export default {
setup() {
// ref() 返回一个包含 .value 属性的响应式对象
const count = ref(0)
const message = ref('Hello Vue 3')
// 在 <script> 中,必须通过 .value 访问或修改其值
console.log(count.value) // 输出: 0
const increment = () => {
count.value++
}
// 将 ref 对象返回,使其在模板中可用
return {
count,
message,
increment
}
}
}<template>
<!-- 在模板中使用时,Vue 会自动“解包” (unwrap),无需 .value -->
<div>{{ count }}</div> <!-- 渲染: 0 -->
<div>{{ message }}</div> <!-- 渲染: Hello Vue 3 -->
<button @click="increment">+1</button>
</template>reactive() - 创建对象的响应式代理
reactive 用于创建一个对象的深度响应式代理。
基本用法
import { reactive } from 'vue'
export default {
setup() {
// reactive() 接收一个对象,并返回其响应式代理
const state = reactive({
count: 0,
user: {
name: 'John',
age: 25
},
items: []
})
const updateUser = () => {
// 可以直接修改属性,无需 .value
state.user.name = 'Jane'
state.items.push({ id: 1, title: 'New Item' })
}
// 返回整个 state 对象
return {
state,
updateUser
}
}
}ref vs reactive 选择指南
ref:推荐用于处理基本数据类型(String, Number, Boolean)。reactive:推荐用于处理对象或数组。
// ✅ 推荐用法,代码语义更清晰
const count = ref(0);
const user = reactive({ name: 'John' });
// ❌ 不推荐
const countAsObject = reactive({ value: 0 }); // 用 reactive 包装基本类型,过于繁琐
const userAsRef = ref({ name: 'John' }); // 用 ref 包装对象,每次访问都需要 .value (userAsRef.value.name)computed() - 计算属性
computed 用于创建一个根据其他响应式数据派生出来的值。它具有缓存特性。
基本用法
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
// 1. 只读的计算属性 (传入一个 getter 函数)
// 只有当 firstName.value 或 lastName.value 变化时才会重新计算
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 2. 可写的计算属性 (传入一个包含 get 和 set 的对象)
const editableName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (newValue) => {
// 当你尝试给 editableName.value 赋值时,set 函数会被调用
const names = newValue.split(' ')
firstName.value = names[0]
lastName.value = names[1] || ''
}
})
return {
fullName,
editableName
}
}
}watch() & watchEffect() - 侦听器
watch:明确指定侦听源
watch 需要明确指定要侦听的数据源,并在数据变化时执行回调函数。
import { ref, reactive, watch } from 'vue'
export default {
setup() {
const count = ref(0)
const state = reactive({ name: 'John' })
// 1. 侦听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 2. 侦听 getter 函数,可以访问对象属性
watch(() => state.name, (newName, oldName) => {
console.log(`name 变化了: ${newName}`)
})
// 3. 侦听多个源
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
console.log('count 或 name 发生了变化')
})
// 4. 选项:deep (深度侦听) 和 immediate (立即执行)
watch(
() => state,
(newState, oldState) => { console.log('state 深度变化') },
{ deep: true, immediate: true } // immediate 会让 watch 在初始化时立即执行一次
)
return { count, state }
}
}watchEffect:自动追踪依赖
watchEffect 会立即执行一次,并自动追踪回调函数中所有使用到的响应式依赖。当任何依赖发生变化时,它会重新运行。
import { ref, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('hello')
// watchEffect 会立即执行,并自动追踪 count 和 message 的变化
watchEffect(() => {
// 在这个函数体内用到了 count.value 和 message.value
// 所以 Vue 会自动侦听这两个响应式引用的变化
console.log(`count: ${count.value}, message: ${message.value}`)
})
// 它等价于一个立即执行 (immediate: true) 且自动依赖收集的 watch
return { count, message }
}
}对比
| 特性 | atch | watchEffect |
|---|---|---|
| 侦听源 | 需要手动指定 | 自动追踪依赖,无需指定 |
| 执行时机 | 默认在数据变化后执行 | 立即执行一次,然后依赖变化后执行 |
| 回调参数 | 可以访问新值和旧值 | 无法访问旧值 |
| 使用场景 | 需要精确控制侦听目标,或需要访问旧值时 | 逻辑简单,只需依赖变化就执行副作用时 |
响应式陷阱与解决方案
陷阱 1:解构 reactive 对象导致失去响应性
直接解构 reactive 对象会使其属性变为普通变量,从而失去响应性。
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({ count: 0, name: 'John' })
// ❌ 错误:count 和 name 只是普通变量,不再具有响应性
// 当 state.count 改变时,这里的 count 不会更新
const { count, name } = state
// ✅ 正确:使用 toRefs()
// toRefs 会将 reactive 对象的每个属性都转换为一个 ref
const stateAsRefs = toRefs(state)
// 现在 stateAsRefs.count 和 stateAsRefs.name 都是 ref,可以在模板中安全使用
return {
// 如果直接返回 count, 它不是响应式的
// 必须返回 toRefs 转换后的结果
...stateAsRefs // 使用扩展运算符将 { count: ref, name: ref } 返回
}
}
}陷阱 2:错误地替换响应式对象
对于 ref 包裹的对象或数组,修改时必须通过 .value 属性进行
import { ref } from 'vue'
export default {
setup() {
let list = ref([1, 2, 3])
const updateList = () => {
// ❌ 错误:这会使 list 变量指向一个新的普通数组,原来的响应式引用丢失了
// list = [4, 5, 6]
// ✅ 正确:始终通过 .value 来修改 ref 的值
list.value = [4, 5, 6]
}
return { list, updateList }
}
}与 React Hooks 对比
| 特性 | Vue Composition API | React Hooks |
|---|---|---|
| 执行时机 | setup 函数只在组件创建时执行一次。 | Hooks 函数在每次组件渲染时都会执行。 |
| 响应式系统 | 基于 Proxy 的响应式系统,数据变更自动触发更新。 | 需要通过 useState 的 set 函数手动更新状态,触发重渲染。 |
| 依赖管理 | computed 和 watchEffect 自动追踪依赖,无需手动声明。 | useMemo, useCallback 需要手动管理依赖数组,否则可能导致性能问题或陈旧闭包。 |
| 心智负担 | 心智负担较低,更接近原生 JavaScript 的编程体验。 | 心智负担较高,需要理解闭包、依赖数组等规则。 |
| 123123123 | 123123123 | 123123123 |
高级应用:自定义组合函数
组合函数是 Composition API 的精髓,它允许我们将可复用的有状态逻辑封装起来。
示例:设计一个 useFetch 组合函数
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'
// 一个组合函数就是一个返回响应式状态的普通函数
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
const fetchData = async () => {
// 重置状态
loading.value = true
error.value = null
try {
// toValue 可以将 ref 或 getter 解包为普通值
const response = await fetch(toValue(url))
if (!response.ok) throw new Error('Network response was not ok')
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// 使用 watchEffect 侦听 URL 的变化
// 如果 url 是一个 ref (例如 computed),当它变化时会自动重新 fetch
watchEffect(fetchData)
// 返回响应式状态和方法
return { data, error, loading, refetch: fetchData }
}使用示例
// MyComponent.vue
import { ref, computed } from 'vue'
import { useFetch } from './composables/useFetch'
export default {
setup() {
const userId = ref(1)
// 创建一个计算属性作为 URL,当 userId 变化时,URL 会自动更新
const url = computed(() => `https://api.example.com/users/${userId.value}`)
// 使用自定义的 useFetch Hook
// 当 url 变化时,useFetch 内部的 watchEffect 会自动重新请求数据
const { data: user, loading, error } = useFetch(url)
const changeUser = () => {
userId.value++ // 这会触发 url 的重新计算,进而触发 useFetch 重新执行
}
return { user, loading, error, changeUser }
}
}