3.Composition API与响应式系统
查看飞书原文 ↗5,834 字符
✏️
Composition API 是 Vue 3 最重要的新特性,也是面试中的高频考点。它不仅改变了我们组织代码的方式,还为逻辑复用提供了更优雅的解决方案。掌握它,你就掌握了 Vue 3 的精髓。
为什么需要 Composition API
核心 API 详解
1. ref() - 创建基础类型的响应式引用
2. reactive() - 创建对象的响应式代理
3. 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
}
}
}
✏️
面试标准回答
computed 用于创建计算属性,它会基于其响应式依赖进行缓存。这意味着只要依赖项没有发生变化,多次访问计算属性会立即返回之前计算过的结果,而不会重新执行计算函数。这是它与 methods 的核心区别。computed 可以是只读的,也可以通过提供 get 和 set 函数来创建可写的计算属性。
4. 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 }
}
}对比
| | |
| | |
执行时机 | 默认在数据变化后执行 | 立即执行一次,然后依赖变化后执行 |
回调参数 | 可以访问新值和旧值 | 无法访问旧值 |
使用场景 | 需要精确控制侦听目标,或需要访问旧值时 | 逻辑简单,只需依赖变化就执行副作用时 |
✏️
面试标准回答
watch 和 watchEffect 都用于在数据变化时执行副作用。主要区别在于:watch 需要显式指定侦听的数据源,让你能精确控制副作用的触发时机,并且可以访问到新值和旧值。而 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 对比
| | |
| | |
| | |
依赖管理 | computed 和 watchEffect 自动追踪依赖,无需手动声明。 | useMemo, useCallback 需要手动管理依赖数组,否则可能导致性能问题或陈旧闭包。 |
心智负担 | 心智负担较低,更接近原生 JavaScript 的编程体验。 | 心智负担较高,需要理解闭包、依赖数组等规则。 |
高级应用:自定义组合函数
组合函数是 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 }
}
}