1. 为什么需要setup()函数
在Vue 2.x时代,我们主要通过data、methods、computed等选项来组织组件逻辑。这种方式在小型组件中表现良好,但随着组件复杂度增加,会出现几个明显问题:
- 相关逻辑被分散到不同选项中
- 大型组件难以阅读和维护
- 逻辑复用困难(虽然可以通过mixins解决,但mixins有其自身问题)
Vue 3的setup()函数正是为了解决这些问题而设计的。它主要有以下优势:
- 逻辑集中:所有组件逻辑都可以写在setup()函数内部
- 更好的类型推断:对TypeScript支持更友好
- 更灵活的逻辑复用:可以通过组合式API实现更灵活的逻辑复用
- 更少的this使用:避免this指向带来的困惑
提示:setup()是组合式API的入口,它替代了Vue 2.x中的大部分选项(data、methods、computed等)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. setup()函数的基本用法
2.1 函数签名与执行时机
setup()函数有两个参数:
javascript复制setup(props, context) {
// 你的代码
}
- props:组件接收的属性,是响应式的
- context:包含attrs、slots、emit等非响应式属性
setup()的执行时机是在beforeCreate和created生命周期之间,此时组件实例尚未创建,因此无法访问this。
2.2 返回值的作用
setup()可以返回一个对象,这个对象的属性会被暴露给模板使用:
javascript复制export default {
setup() {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment
}
}
}
在模板中可以直接使用这些属性和方法:
html复制<template>
<button @click="increment">{{ count }}</button>
</template>
2.3 与选项式API的对比
| 特性 | 选项式API | 组合式API(setup) |
|---|---|---|
| 代码组织 | 按选项类型分组 | 按逻辑功能分组 |
| 复用性 | 依赖mixins | 可提取为组合函数 |
| TypeScript支持 | 一般 | 优秀 |
| 学习曲线 | 平缓 | 较陡峭 |
| 适用场景 | 简单组件 | 复杂组件 |
3. setup()中的响应式处理
3.1 ref与reactive的使用
在setup()中,我们需要显式地创建响应式数据:
javascript复制import { ref, reactive } from 'vue'
setup() {
// 基本类型使用ref
const count = ref(0)
// 对象类型可以使用reactive
const state = reactive({
name: 'Vue',
version: 3
})
return {
count,
state
}
}
注意:ref创建的响应式数据在模板中会自动解包,但在JS中需要通过.value访问
3.2 computed和watch的使用
javascript复制import { computed, watch } from 'vue'
setup() {
const count = ref(0)
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 侦听器
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变为${newVal}`)
})
return {
count,
doubleCount
}
}
3.3 生命周期钩子的使用
在setup()中,生命周期钩子是通过on前缀的函数使用的:
javascript复制import { onMounted, onUpdated } from 'vue'
setup() {
onMounted(() => {
console.log('组件已挂载')
})
onUpdated(() => {
console.log('组件已更新')
})
}
4. setup()的高级用法
4.1 使用provide/inject
javascript复制import { provide, inject } from 'vue'
// 祖先组件
setup() {
provide('theme', 'dark')
}
// 后代组件
setup() {
const theme = inject('theme', 'light') // 默认值'light'
return { theme }
}
4.2 使用模板引用(ref)
javascript复制import { ref, onMounted } from 'vue'
setup() {
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
return {
inputRef
}
}
模板中使用:
html复制<template>
<input ref="inputRef">
</template>
4.3 使用自定义渲染函数
javascript复制import { h } from 'vue'
setup() {
return () => h('div', 'Hello Vue 3!')
}
5. 常见问题与最佳实践
5.1 为什么setup()中不能使用this?
因为setup()在组件实例创建之前执行,此时this尚未指向组件实例。这是设计上的选择,目的是让setup()更加纯粹,不依赖组件实例。
5.2 如何组织大型组件的setup()?
建议将相关逻辑提取为组合函数:
javascript复制// useCounter.js
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment
}
}
// 组件中
import { useCounter } from './useCounter'
setup() {
const { count, increment } = useCounter()
return {
count,
increment
}
}
5.3 setup()中的异步操作
可以在setup()中使用async/await:
javascript复制import { ref } from 'vue'
setup() {
const data = ref(null)
async function fetchData() {
const response = await fetch('/api/data')
data.value = await response.json()
}
fetchData()
return {
data
}
}
5.4 与选项式API混用
虽然可以混用,但不推荐:
javascript复制export default {
setup() {
// 组合式API
},
data() {
// 选项式API
}
}
混用会增加理解难度,建议在新项目中统一使用组合式API。
6. 实际项目中的应用示例
6.1 表单处理
javascript复制import { ref } from 'vue'
setup() {
const form = ref({
username: '',
password: ''
})
const errors = ref({})
function validate() {
errors.value = {}
if (!form.value.username) {
errors.value.username = '用户名不能为空'
}
if (!form.value.password) {
errors.value.password = '密码不能为空'
}
return Object.keys(errors.value).length === 0
}
async function submit() {
if (!validate()) return
try {
await api.login(form.value)
// 登录成功处理
} catch (error) {
// 错误处理
}
}
return {
form,
errors,
submit
}
}
6.2 列表数据加载
javascript复制import { ref, onMounted } from 'vue'
setup() {
const items = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchItems() {
try {
loading.value = true
const response = await api.getItems()
items.value = response.data
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(fetchItems)
return {
items,
loading,
error,
retry: fetchItems
}
}
6.3 组件通信
javascript复制// 父组件
setup() {
const childRef = ref(null)
function callChildMethod() {
childRef.value.doSomething()
}
return {
childRef,
callChildMethod
}
}
// 子组件
setup(props, { expose }) {
function doSomething() {
console.log('子组件方法被调用')
}
expose({
doSomething
})
return {
// ...
}
}
7. 性能优化建议
7.1 避免不必要的响应式
不是所有数据都需要响应式:
javascript复制setup() {
// 不需要响应式的常量
const PI = 3.14
// 需要响应式的数据
const count = ref(0)
return {
PI,
count
}
}
7.2 合理使用computed
计算属性会缓存结果,适合计算开销较大的操作:
javascript复制setup() {
const list = ref([...大量数据...])
const filteredList = computed(() => {
return list.value.filter(item => item.active)
})
return {
filteredList
}
}
7.3 使用shallowRef和shallowReactive
当只需要浅层响应式时:
javascript复制import { shallowRef, shallowReactive } from 'vue'
setup() {
// 不会深度转换
const shallowObj = shallowReactive({ nested: { a: 1 } })
// 不会自动解包
const shallowValue = shallowRef(0)
return {
shallowObj,
shallowValue
}
}
7.4 避免在模板中使用复杂表达式
将复杂逻辑移到setup()中:
javascript复制// 不推荐
<template>
<div>{{ list.filter(...).map(...).slice(...) }}</div>
</template>
// 推荐
setup() {
const list = ref([...])
const processedList = computed(() => {
return list.value.filter(...).map(...).slice(...)
})
return {
processedList
}
}
8. TypeScript支持
8.1 类型定义基础
typescript复制import { defineComponent, ref } from 'vue'
interface User {
id: number
name: string
}
export default defineComponent({
setup() {
const user = ref<User | null>(null)
return {
user
}
}
})
8.2 props类型定义
typescript复制import { defineComponent } from 'vue'
export default defineComponent({
props: {
// 基本类型
count: {
type: Number,
required: true
},
// 复杂类型
user: {
type: Object as PropType<User>,
required: false,
default: () => ({ id: 0, name: 'Guest' })
}
},
setup(props) {
// props会自动推断类型
console.log(props.count.toFixed(2))
}
})
8.3 组合函数类型
typescript复制import { ref, Ref } from 'vue'
interface UseCounterReturn {
count: Ref<number>
increment: () => void
}
export function useCounter(initialValue = 0): UseCounterReturn {
const count = ref(initialValue)
function increment() {
count.value++
}
return {
count,
increment
}
}
9. 测试策略
9.1 单元测试setup()
javascript复制import { mount } from '@vue/test-utils'
import Component from './Component.vue'
test('测试setup()', async () => {
const wrapper = mount(Component, {
props: {
initialCount: 5
}
})
expect(wrapper.vm.count).toBe(5)
await wrapper.vm.increment()
expect(wrapper.vm.count).toBe(6)
})
9.2 测试组合函数
javascript复制import { useCounter } from './useCounter'
import { ref } from 'vue'
test('测试useCounter', () => {
const { count, increment } = useCounter(10)
expect(count.value).toBe(10)
increment()
expect(count.value).toBe(11)
})
9.3 测试异步逻辑
javascript复制import { flushPromises, mount } from '@vue/test-utils'
import AsyncComponent from './AsyncComponent.vue'
test('测试异步setup()', async () => {
const wrapper = mount(AsyncComponent)
expect(wrapper.vm.loading).toBe(true)
await flushPromises()
expect(wrapper.vm.loading).toBe(false)
expect(wrapper.vm.data).toEqual({ /* 预期数据 */ })
})
10. 迁移策略
10.1 从Vue 2迁移
-
渐进式迁移:
- 可以先在Vue 2项目中使用@vue/composition-api插件
- 逐步将组件改写为组合式API
-
常见模式转换:
- data → ref/reactive
- methods → 普通函数
- computed → computed()
- watch → watch()
- 生命周期 → onXxx()
-
工具辅助:
- 使用Vue CLI的迁移助手
- 逐步运行测试确保功能正常
10.2 常见陷阱
-
过度使用ref:
- 对象类型更适合使用reactive
- 模板中ref会自动解包,但JS中需要.value
-
忘记返回值:
- setup()中定义的变量和方法需要return才能在模板使用
-
生命周期混淆:
- created → 直接在setup()中执行代码
- beforeMount → onBeforeMount
- mounted → onMounted
-
响应式丢失:
- 解构reactive对象会失去响应性
- 需要使用toRefs保持响应性
11. 生态系统集成
11.1 与Vue Router集成
javascript复制import { useRouter, useRoute } from 'vue-router'
setup() {
const router = useRouter()
const route = useRoute()
function navigate() {
router.push('/new-route')
}
return {
route,
navigate
}
}
11.2 与Vuex集成
javascript复制import { useStore } from 'vuex'
setup() {
const store = useStore()
const count = computed(() => store.state.count)
function increment() {
store.commit('increment')
}
return {
count,
increment
}
}
11.3 与Pinia集成
javascript复制import { useCounterStore } from '@/stores/counter'
setup() {
const counter = useCounterStore()
return {
counter
}
}
12. 调试技巧
12.1 开发工具支持
Vue DevTools对组合式API有良好支持:
- 可以查看setup()中定义的ref和reactive
- 可以跟踪composition函数的调用
12.2 调试响应式数据
javascript复制import { isRef, isReactive, toRaw } from 'vue'
setup() {
const count = ref(0)
const state = reactive({ a: 1 })
console.log(isRef(count)) // true
console.log(isReactive(state)) // true
console.log(toRaw(state)) // 获取原始对象
return {
count,
state
}
}
12.3 错误追踪
使用onErrorCaptured捕获组件错误:
javascript复制import { onErrorCaptured } from 'vue'
setup() {
onErrorCaptured((err, instance, info) => {
console.error('组件错误:', err)
return false // 阻止错误继续向上传播
})
}
13. 社区资源与学习路径
13.1 官方文档重点
- 组合式API介绍:理解设计动机和核心概念
- 响应式基础:掌握ref和reactive的区别
- 生命周期钩子:了解新的生命周期注册方式
- 依赖注入:provide/inject在组合式API中的使用
13.2 推荐学习资源
- Vue Mastery的Vue 3课程
- Vue School的组合式API教程
- GitHub上的组合式函数集合
- VueUse工具库的源码学习
13.3 进阶学习路径
- 掌握基础组合式API
- 学习编写自定义组合函数
- 研究开源项目中的组合式API实践
- 探索基于组合式API的状态管理方案
14. 未来发展趋势
14.1 组合式API的演进
- 更完善的TypeScript支持:类型推断会越来越智能
- 更多的内置组合函数:Vue核心可能会提供更多实用组合函数
- 更好的开发工具集成:调试体验会进一步提升
14.2 生态系统适配
- 主流库的适配:越来越多的库会提供组合式API的支持
- 新的状态管理方案:基于组合式API的状态管理方案会涌现
- SSR和静态站点生成:组合式API在这些场景下的最佳实践会成熟
14.3 最佳实践的沉淀
随着社区经验的积累,会出现:
- 更成熟的项目结构规范
- 更高效的逻辑复用模式
- 更清晰的性能优化指南
在实际项目中,我发现组合式API特别适合中大型项目,它能显著提高代码的可维护性。刚开始转换时可能会有些不适应,但一旦熟悉后,你会发现它比选项式API更加灵活和强大。建议从新项目开始尝试,逐步积累经验。
