Appearance
Vue3 setup 函数详解
一、概述
setup 是 Vue3 引入的新选项/函数,它是组合式 API 的入口点。setup 在组件创建之前执行,在 props 解析之后、组件实例化之前调用。
二、选项式 API 中的 setup
2.1 基本用法
在选项式 API 中,setup 作为组件的一个选项存在:
javascript
export default {
props: {
title: String
},
setup(props, context) {
// 组合式 API 逻辑
return {
// 返回给模板使用的数据和方法
}
},
data() {
return {
message: 'hello'
}
},
methods: {
handleClick() {
console.log('clicked')
}
},
mounted() {
console.log('mounted')
}
}2.2 选项式 API 中 setup 的 this 指向
this 指向:undefined
javascript
export default {
setup(props, context) {
console.log(this) // undefined
return {}
}
}为什么是 undefined?
执行时机早于组件实例化:setup 函数在组件实例(
this)创建之前执行,此时组件实例尚未生成。设计决策:Vue 团队故意将 this 设为 undefined,原因如下:
- 避免开发者混淆选项式 API 和组合式 API 的使用方式
- 防止在 setup 中错误地访问其他选项(如 data、methods)
- 强制开发者使用组合式 API 的方式(通过返回值暴露数据)
源码层面:在 Vue 源码中,setup 被调用时没有绑定任何 this:
javascript// 简化的源码逻辑 const setupResult = setup(props, setupContext) // setup 被普通调用,而非 call/apply 绑定 this
2.3 选项式 API 中 setup 的执行时机
父组件 beforeCreate
↓
父组件 setup (如果有)
↓
父组件 created
↓
父组件 beforeMount
↓
子组件 beforeCreate
↓
子组件 setup (如果有)
↓
子组件 created
↓
子组件 beforeMount
↓
子组件 mounted
↓
父组件 mountedsetup 执行顺序:在 beforeCreate 之前,created 之前执行。
三、组合式 API 中的 setup
3.1 基本用法(<script setup> 语法糖)
vue
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
const props = defineProps({
title: String
})
const emit = defineEmits(['update', 'close'])
const count = ref(0)
const state = reactive({
name: 'Vue3',
version: '3.x'
})
const doubledCount = computed(() => count.value * 2)
function increment() {
count.value++
emit('update', count.value)
}
watch(count, newVal => {
console.log('count changed:', newVal)
})
onMounted(() => {
console.log('component mounted')
})
</script>
<template>
<div>
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubledCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>3.2 组合式 API 中 setup 的 this 指向
<script setup> 中没有 this
vue
<script setup>
console.log(this) // undefined 或报错(严格模式)
</script>为什么没有 this?
编译时转换:
<script setup>会在编译时被转换为标准的 setup 函数,所有顶层变量自动成为暴露给模板的属性。设计理念:组合式 API 的核心思想是:
- 使用函数而非 this 来访问响应式系统
- 通过 import 导入 API(ref、reactive、computed 等)
- 不依赖组件实例上下文
编译后的代码:
javascript// 编译前 ;<script setup>const count = ref(0)</script> // 编译后(简化) export default { setup() { const count = ref(0) return { count } } }
3.3 标准 setup 函数语法
vue
<script>
import { ref, reactive } from 'vue'
export default {
props: {
title: String
},
setup(props, context) {
const count = ref(0)
// context 包含:attrs, slots, emit, expose
const { attrs, slots, emit, expose } = context
function handleClick() {
emit('click', count.value)
}
return {
count,
handleClick
}
}
}
</script>四、setup 函数做了什么事情
4.1 核心职责
| 职责 | 说明 |
|---|---|
| 初始化响应式状态 | 创建 ref、reactive 等响应式数据 |
| 定义计算属性 | 使用 computed 创建派生状态 |
| 设置侦听器 | 使用 watch/watchEffect 监听数据变化 |
| 注册生命周期钩子 | 使用 onMounted、onUnmounted 等 |
| 组织代码逻辑 | 将相关逻辑组合在一起,提高可维护性 |
| 返回模板需要的内容 | 返回数据和方法供模板使用 |
4.2 执行流程详解
javascript
setup(props, context) {
// 1. 接收 props(响应式的)
console.log(props.title)
// 2. 解构 context
const { attrs, slots, emit, expose } = context
// 3. 创建响应式数据
const state = reactive({ count: 0 })
const message = ref('hello')
// 4. 创建计算属性
const doubled = computed(() => state.count * 2)
// 5. 设置侦听器
watch(() => state.count, (newVal) => {
console.log('count changed')
})
// 6. 注册生命周期
onMounted(() => {
console.log('mounted')
})
onUnmounted(() => {
console.log('unmounted')
})
// 7. 定义方法
function increment() {
state.count++
}
// 8. 使用 expose 暴露给父组件
expose({
increment,
state
})
// 9. 返回给模板
return {
state,
message,
doubled,
increment
}
}4.3 setup 参数详解
props
javascript
setup(props) {
// props 是响应式的,不能解构(会失去响应性)
console.log(props.title)
// 如需解构,使用 toRefs
import { toRefs } from 'vue'
const { title } = toRefs(props)
// 或 toRef 单个属性
import { toRef } from 'vue'
const title = toRef(props, 'title')
}context
javascript
setup(props, context) {
// attrs: 非响应式对象,包含父组件传递的非 prop 属性
console.log(context.attrs.class)
console.log(context.attrs.id)
// slots: 非响应式对象,包含插槽
console.log(context.slots.default)
// emit: 触发事件的函数
context.emit('update', newValue)
// expose: 暴露组件公共属性
context.expose({
publicMethod,
publicProperty
})
}五、选项式 API 与组合式 API 中 setup 的区别
为什么 setup 里 this 是 undefined? 因为: setup 执行时机非常早(在组件创建之前) 此时组件实例还没生成,所以 this 没有指向 Vue 故意把 this 设为 undefined,避免你误用;
非要拿实例使用 getCurrentInstance()
| 特性 | 选项式 API 中的 setup | 组合式 API (<script setup>) |
|---|---|---|
| 语法 | 作为选项之一 | 语法糖,编译后生成 setup 函数 |
| this | undefined | undefined (无 this 概念 ) |
| 返回值 | 必须显式 return | 自动暴露顶层变量 |
| props 定义 | 在 props 选项中 | 使用 defineProps() |
| emit 定义 | 在 emits 选项中 | 使用 defineEmits() |
| 与选项共存 | 可与 data、methods 等共存 | 独立使用 |
| 代码组织 | 分散在各选项中 | 逻辑可集中组织 |
六、使用注意事项
6.1 this 相关注意事项
javascript
export default {
setup(props, context) {
// ❌ 错误:无法访问 this
console.log(this.data) // undefined
console.log(this.message) // undefined
// ❌ 错误:无法调用 methods
this.handleClick() // TypeError
// ❌ 错误:无法访问组件实例属性
this.$router // undefined
this.$route // undefined
// ✅ 正确:使用组合式 API 方式
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
},
data() {
return {
message: 'hello'
}
},
methods: {
handleClick() {
// ✅ 这里可以访问 setup 返回的数据
console.log(this.count) // 如果 setup 返回了 count
}
}
}6.2 props 使用注意事项
javascript
setup(props) {
// ❌ 错误:直接解构会失去响应性
const { title } = props
// ✅ 正确:使用 toRefs
import { toRefs, toRef } from 'vue'
const { title } = toRefs(props)
// ✅ 正确:使用 toRef
const title = toRef(props, 'title')
// ❌ 错误:修改 props
props.title = 'new title' // 警告,不应修改 props
// ✅ 正确:创建本地副本
const localTitle = ref(props.title)
}6.3 生命周期钩子注意事项
javascript
setup() {
// ❌ 错误:在 setup 之外调用钩子
onMounted(() => {}) // 必须在 setup 内同步调用
// ❌ 错误:在异步回调中调用
setTimeout(() => {
onMounted(() => {}) // 错误!
}, 100)
// ✅ 正确:在 setup 内同步注册
onMounted(() => {
console.log('mounted')
})
// ✅ 正确:异步操作放在钩子内
onMounted(async () => {
const data = await fetchData()
})
}6.4 返回值注意事项
javascript
setup() {
const count = ref(0)
const state = reactive({ name: 'Vue' })
// ❌ 错误:返回 reactive 对象会失去响应性
return { ...state }
// ✅ 正确:返回 ref
return { count }
// ✅ 正确:返回 reactive 对象本身
return { state }
// ✅ 正确:使用 toRefs
return { ...toRefs(state) }
// ❌ 错误:返回函数会变成方法而非渲染函数
return () => 'render' // 这会作为方法,不是渲染函数
// ✅ 正确:返回渲染函数
import { h } from 'vue'
return () => h('div', count.value)
}6.5 与选项式 API 混用注意事项
javascript
export default {
props: ['title'],
setup(props) {
const count = ref(0)
return { count }
},
data() {
return {
message: 'hello'
}
},
computed: {
// ✅ 可以访问 setup 返回的数据
doubledCount() {
return this.count * 2
}
},
methods: {
increment() {
// ✅ 可以访问 setup 返回的数据
this.count++
}
},
mounted() {
// ✅ 可以访问 setup 返回的数据
console.log(this.count)
}
}注意:setup 返回的数据在选项式 API 中可以访问,但选项式 API 的数据在 setup 中无法访问。
6.6 响应式丢失问题
javascript
import { ref, reactive, toRefs, toValue } from 'vue'
setup() {
const count = ref(0)
const state = reactive({ name: 'Vue', version: 3 })
// ❌ 响应式丢失场景
const { name } = state // 失去响应性
const countValue = count.value // 失去响应性
// ✅ 保持响应性
const { name } = toRefs(state)
const countRef = count // 保持 ref
// 传递给子组件时
// ❌ 可能丢失响应性
provide('state', state.name)
// ✅ 正确传递
provide('state', toRef(state, 'name'))
provide('state', state) // 传递整个对象
}6.7 异步 setup(Suspense)
javascript
// 异步 setup 需要配合 Suspense 使用
async setup() {
const data = await fetchUserData()
return { data }
}
// 父组件
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>6.8 expose 使用注意事项
javascript
setup(props, { expose }) {
const count = ref(0)
const privateData = ref('secret')
function increment() {
count.value++
}
function privateMethod() {
console.log('private')
}
// 只暴露指定的内容给父组件
expose({
count,
increment
// privateData 和 privateMethod 不会被暴露
})
return {
count,
increment,
privateData,
privateMethod
}
}
// 父组件通过 ref 访问
const childRef = ref()
childRef.value.count // ✅ 可以访问
childRef.value.increment() // ✅ 可以调用
childRef.value.privateData // ❌ undefined七、最佳实践
7.1 代码组织
javascript
// ✅ 推荐:按功能组织代码
setup() {
// 用户相关逻辑
const { user, login, logout } = useUser()
// 购物车相关逻辑
const { cart, addToCart, removeFromCart } = useCart()
// UI 相关逻辑
const { isOpen, open, close } = useModal()
return {
user, login, logout,
cart, addToCart, removeFromCart,
isOpen, open, close
}
}
// ✅ 推荐:提取为组合式函数
function useUser() {
const user = ref(null)
async function login(credentials) {
user.value = await api.login(credentials)
}
function logout() {
user.value = null
}
return { user, login, logout }
}7.2 命名规范
javascript
setup() {
// ✅ ref 使用语义化命名
const isLoading = ref(false)
const hasError = ref(false)
const userList = ref([])
// ✅ reactive 用于对象状态
const form = reactive({
username: '',
password: ''
})
// ✅ computed 使用描述性命名
const filteredUsers = computed(() =>
userList.value.filter(u => u.active)
)
// ✅ 方法使用动词开头
function handleSubmit() {}
function handleCancel() {}
return {
isLoading,
hasError,
userList,
form,
filteredUsers,
handleSubmit,
handleCancel
}
}7.3 TypeScript 支持
typescript
import { ref, reactive, computed, type Ref, type ComputedRef } from 'vue'
interface User {
id: number
name: string
email: string
}
setup() {
// 类型推断
const count = ref(0) // Ref<number>
const user = ref<User | null>(null) // Ref<User | null>
// reactive 类型
const state = reactive<{
users: User[]
loading: boolean
}>({
users: [],
loading: false
})
// computed 类型
const userCount: ComputedRef<number> = computed(() => state.users.length)
return {
count,
user,
state,
userCount
}
}八、总结
setup 函数核心要点
| 要点 | 说明 |
|---|---|
| 执行时机 | 在 beforeCreate 之前,组件实例化之前 |
| this 指向 | undefined(设计如此,避免混淆) |
| 参数 | props(响应式)和 context(包含 attrs、slots、emit、expose) |
| 返回值 | 对象(暴露给模板)或渲染函数 |
| 核心作用 | 组合式 API 的入口,组织组件逻辑 |
选项式 vs 组合式 setup 选择
- 选项式 API + setup:渐进式迁移,与现有代码兼容
- 组合式 API(
<script setup>):新项目推荐,代码更简洁,TypeScript 支持更好
记住这些规则
- setup 中没有 this,不要尝试访问
- props 不要直接解构,使用 toRefs
- 生命周期钩子必须在 setup 中同步调用
- 返回渲染函数时,不要返回其他属性
- 使用组合式函数组织复杂逻辑