3.Composition API与响应式系统 来源:https://my.feishu.cn/docx/H4ejdDoYGoTulyxFFPoctqaDnfh 采集状态:已完成正文采集;已滚动到底部并复查 ✏️ 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 都用于在数据变化时执行副作用。主要区别在于: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 对比​ 特性 Vue Composition API React Hooks 执行时机 setup 函数只在组件创建时执行一次。 Hooks 函数在每次组件渲染时都会执行。 响应式系统 基于 Proxy 的响应式系统,数据变更自动触发更新。 需要通过 useState 的 set 函数手动更新状态,触发重渲染。 依赖管理 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 } } } ​