公司动态

Vue 3 组合式 API 核心概念与实战指南:从响应式基础到逻辑复用

📅 2026/8/17 8:49:11
Vue 3 组合式 API 核心概念与实战指南:从响应式基础到逻辑复用
1. 项目概述从“选项式”到“组合式”的范式跃迁如果你是从 Vue 2 或者早期 Vue 3 的选项式 API 一路走过来的开发者第一次接触“组合式 API”这个概念时可能会有点懵。它不像data、methods、computed那样把功能分门别类地放在固定的“选项”格子里而是让你用一种更自由、更像编写普通 JavaScript 函数的方式来组织组件的逻辑。简单来说组合式 API 是一套基于函数的 API它允许你在 Vue 组件中更灵活地组合和复用有状态的逻辑。它的核心是setup()函数和一系列以ref、reactive、computed、watch等开头的响应式 API。为什么需要它回想一下在选项式 API 中一个功能相关的代码比如一个用户搜索可能会被拆散查询关键词在data里搜索方法在methods里搜索结果的计算属性在computed里监听输入变化的逻辑在watch里。当组件逻辑复杂后阅读和维护就变成了在几个选项块之间来回跳转的“拼图游戏”。组合式 API 解决了这个问题它让你可以把与同一个功能相关的所有代码状态、计算属性、方法、副作用组合在一起放在同一个地方。这不仅让单个组件的逻辑更内聚、更清晰更重要的是它让提取和复用逻辑变得无比简单——你可以轻松地把一组相关的响应式状态和函数抽离成一个“组合式函数”然后在任何组件中像乐高积木一样引入和使用。对于新手你可以把它理解为选项式 API 是 Vue 给你的一套固定格式的填空题你必须把答案填在指定的位置而组合式 API 是给你一盒功能各异的积木块ref,computed,watchEffect等你可以按照自己的思路自由地搭建出任何你想要的组件逻辑结构。这套 API 尤其适合构建中大型应用、需要高度复用逻辑的库、或者团队协作项目它能显著提升代码的可读性、可维护性和可测试性。2. 核心概念与响应式基础深度解析要玩转组合式 API必须彻底理解它的几个基石ref、reactive以及setup函数的执行上下文。很多人一开始会混淆ref和reactive的使用场景其实区分它们的关键在于你打算如何“持有”这个响应式数据。2.1refvsreactive如何选择你的状态容器ref用于定义一个响应式的引用。它接受一个内部值返回一个响应式的、可变的 ref 对象这个对象只有一个.value属性指向该内部值。在模板中Vue 会自动“解包”所以你直接使用变量名即可无需.value但在 JavaScript 中你必须通过.value来访问和修改它。import { ref } from vue const count ref(0) // 创建一个包含值 0 的 ref 对象 console.log(count.value) // 0 在 JS 中访问需要 .value count.value // 修改值触发响应式更新 // 在模板中直接使用 {{ count }} 无需 .valuereactive则用于创建一个响应式的对象。它返回一个对象的响应式代理。整个被返回的代理对象是响应式的你可以直接访问和修改其属性。import { reactive } from vue const user reactive({ name: 张三, age: 25 }) console.log(user.name) // 张三 直接访问属性 user.age 26 // 直接修改属性触发响应式更新选择策略与核心心法基本类型用ref字符串、数字、布尔值等。因为reactive的代理是针对对象的对基本类型无效。单一对象用reactive当你有一个逻辑上是一个整体的对象比如表单数据、配置对象使用reactive更直观访问属性无需.value。数组和未来可能被替换的引用用ref这是关键点。如果你定义了一个数组const list reactive([])当你试图重新赋值list newArray时会丢失响应性。而const list ref([])允许你通过list.value newArray安全地替换整个数组。同样如果一个对象引用在未来可能被整体替换例如从接口获取一套全新的配置也优先使用ref。组合式函数返回值用ref这是一个最佳实践。组合式函数返回ref可以让使用者通过.value明确知道这是一个响应式引用并且在解构时能保持响应性配合toRefs。注意直接解构reactive对象会丢失响应性const { name, age } user解构出的name和age将是普通的字符串和数字。如果需要解构请使用toRefsconst { name, age } toRefs(user)此时name和age将是ref对象。2.2setup()函数组合式 API 的舞台setup()函数是组合式 API 的“舞台”所有组合式逻辑都在这里编写。它有两个参数props组件接收的 props是响应式的。context一个普通对象暴露了三个组件属性attrs非 props 的属性、slots插槽、emit触发事件的方法。setup()的执行时机在beforeCreate和created生命周期之间。此时组件实例尚未被创建因此你无法访问this。所有原本需要通过this访问的选项如data、computed、methods都需要在setup()内部通过导入的 API 来定义。setup()的返回值至关重要。它返回一个对象这个对象的所有属性都会被暴露给模板和组件的其他选项如通过this访问。通常你会返回在setup()中定义的状态和方法。import { ref } from vue export default { props: [title], setup(props, context) { const count ref(0) const increment () { count.value context.emit(count-changed, count.value) // 使用 context.emit 触发事件 } // 返回的内容可以在模板中使用 return { count, increment, // 也可以选择性地暴露 context 的属性 attrs: context.attrs, slots: context.slots, } } }在 Vue 3.2 中更推荐使用script setup语法糖它让写法更加简洁我们会在后面详细探讨。2.3 计算属性与侦听器响应式逻辑的延伸computed用于声明一个依赖于其他响应式状态的计算属性。它返回一个只读的 ref 对象。import { ref, computed } from vue const firstName ref(张) const lastName ref(三) // 一个计算属性 ref const fullName computed(() { return ${firstName.value} ${lastName.value} }) // fullName.value 是 张 三且当 firstName 或 lastName 变化时自动更新。watch和watchEffect用于响应式地执行“副作用”如数据变化时发起请求、操作 DOM 等。watch需要明确指定要侦听的数据源和一个回调函数。它更精确可以获取变化前后的值。import { ref, watch } from vue const count ref(0) watch(count, (newValue, oldValue) { console.log(count 从 ${oldValue} 变成了 ${newValue}) })watchEffect立即运行一个函数并响应式地追踪其依赖依赖变更时重新执行。它更简洁但一开始就会执行一次。import { ref, watchEffect } from vue const count ref(0) watchEffect(() { console.log(count 的值是${count.value}) // 自动追踪 count }) // 立即打印count 的值是0实操心得对于需要根据一个或多个状态变化来执行异步操作如搜索的场景watch配合immediate选项或watchEffect是标配。但要注意watchEffect的依赖是自动收集的如果回调函数中有条件分支可能会造成依赖追踪不准确。对于明确的、需要防抖的侦听使用watch更可控。3. 生命周期与依赖注入在组合式世界中的对接在选项式 API 中生命周期钩子如mounted、updated是作为选项直接定义的。在组合式 API 中它们以函数的形式提供需要在setup()或script setup中同步调用。3.1 生命周期钩子的使用所有生命周期钩子都以on前缀开头例如onMounted、onUpdated、onUnmounted等。它们接受一个回调函数。import { onMounted, onUnmounted, ref } from vue const timer ref(null) const count ref(0) onMounted(() { console.log(组件挂载了) timer.value setInterval(() { count.value }, 1000) }) onUnmounted(() { console.log(组件即将卸载) if (timer.value) clearInterval(timer.value) })一个重要区别组合式 API 的生命周期钩子没有beforeCreate和created的对应项因为setup()函数本身就是在它们之间运行的。所有原本在created中进行的初始化逻辑都应该直接放在setup()函数的顶层执行。3.2 依赖注入provide与inject在 Vue 中provide和inject用于实现跨层级的组件通信通常用于祖先组件向后代组件传递数据或方法无需通过 props 层层传递。在组合式 API 中它们同样以函数形式提供。在祖先组件中import { provide, ref } from vue const theme ref(dark) provide(theme, theme) // 提供响应式数据 provide(changeTheme, (newTheme) { theme.value newTheme }) // 提供方法在任何后代组件中import { inject } from vue const theme inject(theme, light) // 第二个参数是默认值 const changeTheme inject(changeTheme) // 现在可以使用 theme 和 changeTheme 了注意为了保持响应性建议提供ref或reactive对象。如果提供的是普通值接收方将无法感知其变化。同时为inject提供一个明确的默认值是个好习惯可以提高代码的健壮性。4.script setup语法糖极致的开发体验script setup是在单文件组件中使用组合式 API 的编译时语法糖。它让代码变得极其简洁是当前 Vue 3 开发的绝对主流写法。4.1 基本用法与优势在单文件组件的script标签上添加setup属性其内部的代码会被编译成组件setup()函数的内容。这意味着顶层绑定变量、函数、import 引入自动暴露给模板。无需return语句。更好的 TypeScript 类型推断。script setup import { ref, onMounted } from vue import MyChildComponent from ./MyChildComponent.vue // 导入的组件自动注册 // 状态和函数 const msg ref(Hello World!) const count ref(0) function increment() { count.value } // 生命周期 onMounted(() { console.log(组件已挂载) }) /script template div h1{{ msg }}/h1 button clickincrement点击了 {{ count }} 次/button !-- MyChildComponent 可以直接使用 -- MyChildComponent / /div /template4.2 定义 Props 和 Emits在script setup中使用defineProps和defineEmits编译器宏来声明 props 和 emits。它们具有完整的类型推断支持。script setup // 定义 props const props defineProps({ title: String, likes: { type: Number, default: 0 } }) // 或者使用 TypeScript 泛型更推荐 // const props defineProps{ title: string; likes?: number }() // 定义 emits const emit defineEmits([update:title, submit]) // 或者使用类型字面量 // const emit defineEmits{ (e: update:title, value: string): void; (e: submit): void }() function handleClick() { emit(update:title, 新标题) emit(submit) } /script4.3 暴露组件实例方法defineExpose默认情况下使用script setup的组件是关闭的——即父组件无法通过模板 ref 访问到子组件内部定义的任何东西。如果需要暴露特定的属性或方法需要使用defineExpose编译器宏。子组件ChildComp.vuescript setup import { ref } from vue const a ref(1) const b ref(2) const publicMethod () { console.log(公共方法被调用) } // 只有被 expose 的才能被父组件访问 defineExpose({ a, publicMethod }) /script父组件script setup import { ref, onMounted } from vue import ChildComp from ./ChildComp.vue const childRef ref(null) onMounted(() { console.log(childRef.value?.a) // 1 childRef.value?.publicMethod() // 公共方法被调用 // console.log(childRef.value?.b) // undefined 因为 b 未被暴露 }) /script template ChildComp refchildRef / /template实操心得script setup极大地简化了开发几乎应成为所有新项目的默认选择。它的“自动暴露”特性让代码更干净但也要注意其“默认关闭”的特性合理使用defineExpose来控制组件的公共接口。5. 组合式函数逻辑复用的终极武器组合式函数是组合式 API 的精髓所在也是其命名的由来。它让你能够将组件逻辑提取到可重用的函数中。5.1 如何编写一个组合式函数一个组合式函数就是一个利用 Vue 组合式 API 来封装和复用有状态逻辑的 JavaScript 函数。按照惯例它的名字以use开头。假设我们有一个跟踪鼠标位置的需求可以将其抽象成useMouse// useMouse.js import { ref, onMounted, onUnmounted } from vue export function useMouse() { const x ref(0) const y ref(0) function update(event) { x.value event.pageX y.value event.pageY } onMounted(() window.addEventListener(mousemove, update)) onUnmounted(() window.removeEventListener(mousemove, update)) // 返回响应式状态 return { x, y } }在组件中使用script setup import { useMouse } from ./useMouse.js const { x, y } useMouse() /script template 鼠标位置{{ x }}, {{ y }} /template5.2 接收参数与返回响应式状态组合式函数可以接收参数使其更加灵活。同时返回响应式状态通常是ref是关键。// useFetch.js import { ref, watchEffect } from vue export function useFetch(url) { const data ref(null) const error ref(null) const isLoading ref(false) async function doFetch() { isLoading.value true data.value null error.value null try { const response await fetch(url.value || url) // 支持 ref 或普通值 data.value await response.json() } catch (err) { error.value err } finally { isLoading.value false } } // 如果 url 是 ref则监听其变化 if (isRef(url)) { watchEffect(doFetch) } else { doFetch() } return { data, error, isLoading, retry: doFetch } }5.3 组合式函数的最佳实践命名约定以use开头例如useUser、useWindowSize。输入灵活性参数可以接受ref或普通值。内部使用unref()或判断isRef()来处理如上面的useFetch示例。返回ref对象这允许在使用方进行解构时保持响应性。返回一个由ref组成的普通对象是最佳模式。副作用管理如果组合式函数设置了副作用如事件监听器、定时器务必在onUnmounted中清理。可以返回一个清理函数让使用者手动调用。单一职责一个组合式函数只做一件事并把它做好。useMouse只关心鼠标位置useFetch只关心数据获取。通过组合式函数你可以构建出一套属于自己项目或团队的、高度可复用的逻辑库这是选项式 API 时代难以企及的开发体验。6. 与选项式 API 的对比及迁移策略理解了组合式 API 的强大之后一个很自然的问题是我现有的 Vue 2/选项式 API 项目怎么办需要重写吗答案是不需要但可以渐进式迁移。6.1 核心差异对比特性选项式 API组合式 API代码组织按选项类型组织 (data,methods,computed)。功能逻辑分散。按功能/逻辑组织。相关代码集中在一起。逻辑复用Mixins存在命名冲突、来源不清晰问题或作用域插槽。组合式函数清晰、灵活、类型安全。TypeScript 支持需要额外的类型标注支持度一般。一流的类型推断支持开发体验极佳。this上下文强依赖this在复杂高阶组件或组合中可能指向不明。无this所有函数都是闭包访问的变量来源明确。学习曲线对于熟悉面向对象或 Vue 2 的开发者更直观。需要理解响应式 API 和函数式编程思想初期有一定门槛。灵活性相对固定受限于 Vue 的选项结构。极高可以像编写普通 JS 一样自由组织代码。6.2 渐进式迁移指南Vue 3 完全支持在同一个组件中混用选项式 API 和组合式 API。这为渐进式迁移提供了可能。在新组件中直接使用组合式 API对于新功能或新页面毫不犹豫地使用script setup和组合式 API。在旧组件中局部引入组合式逻辑对于一个庞大的选项式组件可以先将其中的一部分独立逻辑比如一个复杂的表单验证、一个数据获取流程提取成组合式函数useXxx然后在原组件的setup()选项中调用它并将返回的状态映射到选项上虽然有些绕但可行。这能让你立即受益于逻辑复用而无需重写整个组件。// 旧组件.vue import { useFeature } from ./useFeature.js export default { setup() { const { someState, someMethod } useFeature() return { someState, someMethod } // 暴露给选项式 API }, data() { return { // 原有的 data... } }, computed: { // 原有的 computed... 现在也可以使用从 setup 返回的 someState derivedFromSomeState() { return this.someState * 2 } }, methods: { // 原有的 methods... 现在也可以调用从 setup 返回的 someMethod existingMethod() { this.someMethod() } } }逐步重构复杂组件当某个选项式组件变得难以维护时可以将其整体重写为组合式 API 组件。由于逻辑已经被组合式函数模块化重写过程往往会比想象中简单。利用工具社区有一些迁移辅助工具和指南但手动渐进式迁移通常是最可控的方式。迁移心法不要追求一步到位。组合式 API 的优势在长期维护和复杂应用中才会完全显现。从小的、独立的逻辑开始提取组合式函数让团队逐渐熟悉新模式是最稳妥的策略。7. 常见问题与实战避坑指南在实际开发中从选项式转向组合式难免会遇到一些“坑”。这里记录了一些高频问题和解决方案。7.1 响应式丢失问题这是新手最常遇到的问题表现为数据更新了但视图不更新。解构reactive对象如前所述直接解构会丢失响应性。使用toRefs。// 错误 const state reactive({ a: 1, b: 2 }) let { a, b } state // a, b 不再是响应式的 // 正确 const state reactive({ a: 1, b: 2 }) const { a, b } toRefs(state) // a, b 是 ref将ref赋值给reactive对象的属性Vue 会自动解包.value通常没问题。但如果你后续需要替换整个ref需要注意。const count ref(0) const obj reactive({ count }) // obj.count 是 count.value 的代理访问无需 .value console.log(obj.count) // 0 count.value 1 console.log(obj.count) // 1 响应性保持在异步回调中访问响应式变量确保你访问的是.value对于ref或代理对象本身。在setTimeout、Promise、事件监听器的回调中直接写变量名可能访问的是闭包捕获的旧值或非响应式值。7.2watch与watchEffect的陷阱watch监听reactive对象的属性需要用一个 getter 函数。const obj reactive({ a: 1 }) // 错误直接监听 obj.a 可能无法触发 // watch(obj.a, (val) { ... }) // 正确使用 getter 函数 watch(() obj.a, (val) { ... }) // 或者监听整个对象并指定 deep: true性能开销大 watch(obj, (newVal) { ... }, { deep: true })watchEffect的依赖收集时机watchEffect在每次运行前都会清理并重新收集依赖。如果副作用函数中有条件判断依赖可能时有时无导致行为不稳定。对于明确的依赖使用watch更可靠。清理副作用watch和watchEffect都返回一个停止侦听的函数。在组件卸载或不再需要侦听时应调用该函数。const stopWatch watch(source, callback) onUnmounted(() stopWatch()) // 或者 watchEffect const stopEffect watchEffect(() { ... }) onUnmounted(() stopEffect())7.3script setup下的注意事项顶层awaitscript setup中可以使用顶层await代码会被编译成async setup()。但注意这会使该组件自动成为一个异步依赖组件可能需要配合Suspense使用或由父组件处理加载状态。访问组件实例在script setup中无法直接访问this。如果需要访问组件实例通常很少需要可以使用getCurrentInstance()API但此 API 主要用于高级库开发生产代码应尽量避免使用。与普通script共存一个.vue文件可以同时有script setup和普通的script。普通script通常用于声明组件选项如name、inheritAttrs等这些选项无法在script setup中声明。script export default { name: MyComponent, inheritAttrs: false, // 自定义选项 customOption: foo } /script script setup // 组合式 API 逻辑 /script7.4 性能优化相关computed缓存computed值会基于其依赖进行缓存只有依赖变化时才重新计算。确保计算函数是纯函数且没有副作用。shallowRef与shallowReactive如果你确定一个对象的深层结构不需要响应式或者你希望自己控制深层更新的时机可以使用它们来创建浅层响应式对象避免不必要的性能开销。避免在模板中频繁调用方法如果模板中有一个表达式依赖于一个复杂计算应该使用computed而不是在模板中调用方法。因为模板每次重新渲染都会调用方法而computed有缓存。!-- 不佳 -- div{{ expensiveCalculation() }}/div !-- 更佳 -- div{{ expensiveValue }}/div script setup const expensiveValue computed(() expensiveCalculation()) /script掌握这些常见问题的应对之策能让你在组合式 API 的开发中更加游刃有余。归根结底组合式 API 带来的是一种思维模式的转变——从“按选项分类”转向“按功能组合”。一旦适应你将获得前所未有的代码组织能力和开发效率。