Vue 3 setup函数深度解析
1. setup函数概述
setup函数是Vue 3组合式API的核心入口,它是组件内部使用组合式API的起点。setup函数在组件创建之前执行,用于初始化组件的状态、计算属性、方法等,并将它们暴露给模板使用。
1.1 setup函数的基本语法
setup函数的基本语法如下:
export default {
setup(props, context) {
// 组件逻辑
return {
// 暴露给模板的属性
}
}
}1.2 setup函数的执行时机
setup函数的执行时机非常重要,它在组件生命周期中的位置如下:
- 组件实例创建之前执行
- 在
beforeCreate钩子之前执行 - 在
created钩子之前执行 - 只执行一次,不会在组件更新时重新执行
1.3 setup函数的this指向
在setup函数内部,this的指向是undefined,这是有意设计的,目的是:
- 避免与选项式API中的
this混淆 - 鼓励使用组合式API的方式访问组件属性
- 更好地支持TypeScript类型推断
2. setup函数的参数
setup函数接收两个参数:props和context。
2.1 props参数
props参数是一个响应式对象,包含了组件接收的所有props。它具有以下特点:
- 响应式:当props发生变化时,setup函数内部使用props的地方会自动更新
- 只读:不能直接修改props的值,否则会触发警告
- 类型安全:在TypeScript环境下,props会自动进行类型检查
export default {
props: {
message: {
type: String,
default: 'Hello'
},
count: {
type: Number,
default: 0
}
},
setup(props) {
console.log(props.message) // 访问props
console.log(props.count) // 访问props
// 错误:不能直接修改props
// props.count = 10
return {
// ...
}
}
}2.2 context参数
context参数是一个普通对象,包含了组件的上下文信息,它具有以下属性:
- attrs:包含了组件接收的所有非props属性
- slots:包含了组件的所有插槽内容
- emit:用于触发组件的自定义事件
- expose:用于暴露组件的公共方法,供父组件通过ref访问
context对象不是响应式的,可以使用解构赋值:
export default {
setup(props, { attrs, slots, emit, expose }) {
// 使用attrs
console.log(attrs.class)
// 使用emit触发事件
const handleClick = () => {
emit('custom-event', 'event data')
}
// 使用expose暴露公共方法
expose({
publicMethod() {
console.log('This is a public method')
}
})
return {
handleClick
}
}
}2.2.1 attrs属性
attrs是一个普通对象,包含了组件接收的所有非props属性。它具有以下特点:
- 非响应式:attrs不是响应式的,当父组件传递的非props属性变化时,attrs不会自动更新
- 可以修改:可以直接修改attrs的值,不会触发警告
- 包含所有非props属性:包括HTML属性和自定义属性
2.2.2 slots属性
slots是一个普通对象,包含了组件的所有插槽内容。它具有以下特点:
- 非响应式:slots不是响应式的,当插槽内容变化时,slots不会自动更新
- 包含所有插槽:包括默认插槽、具名插槽和作用域插槽
- 需要在渲染时访问:通常在render函数或setup函数返回的渲染函数中使用
2.2.3 emit属性
emit是一个函数,用于触发组件的自定义事件。它的用法与选项式API中的this.$emit类似:
// 触发自定义事件
emit('event-name', eventData)
// 触发带修饰符的v-model事件
emit('update:modelValue', newValue)2.2.4 expose属性
expose是一个函数,用于暴露组件的公共方法,供父组件通过ref访问。它的用法如下:
// 在子组件中
export default {
setup(props, { expose }) {
const privateMethod = () => {
console.log('This is a private method')
}
const publicMethod = () => {
console.log('This is a public method')
privateMethod()
}
// 只暴露publicMethod
expose({
publicMethod
})
return {
// ...
}
}
}
// 在父组件中
<template>
<ChildComponent ref="childRef" />
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const childRef = ref(null)
onMounted(() => {
// 可以访问子组件暴露的publicMethod
childRef.value.publicMethod()
// 不能访问子组件的privateMethod
// childRef.value.privateMethod() // 报错
})
return {
childRef
}
}
}
</script>3. setup函数的返回值
setup函数可以返回两种类型的值:
- 对象:返回的对象会被合并到组件的渲染上下文,供模板使用
- 渲染函数:直接返回一个渲染函数,用于渲染组件
3.1 返回对象
返回对象是setup函数最常用的方式,返回的对象中的属性会被暴露给模板使用:
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => {
count.value++
}
return {
count,
double,
increment
}
}
}在模板中,可以直接使用这些属性:
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ double }}</p>
<button @click="increment">Increment</button>
</div>
</template>3.2 返回渲染函数
setup函数也可以返回一个渲染函数,直接控制组件的渲染:
import { h, ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return () => h('div', [
h('p', `Count: ${count.value}`),
h('button', { onClick: increment }, 'Increment')
])
}
}这种方式通常用于需要动态生成组件结构的场景,或者与JSX一起使用:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return () => (
<div>
<p>Count: {count.value}</p>
<button onClick={increment}>Increment</button>
</div>
)
}
}3.3 注意事项
- 如果setup函数返回对象,那么对象中的属性必须是响应式的,否则模板不会自动更新
- 如果setup函数返回渲染函数,那么它将忽略组件的template选项
- 不要在setup函数中返回
undefined,否则模板将无法访问任何属性
4. setup函数与响应式系统
setup函数是组合式API与响应式系统结合的核心,它通过以下API来创建和管理响应式数据:
4.1 使用ref创建响应式数据
ref用于创建基本类型的响应式数据:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0) // 创建响应式数据
const message = ref('Hello') // 创建响应式字符串
const increment = () => {
count.value++ // 访问和修改ref值需要使用.value
}
return {
count,
message,
increment
}
}
}4.2 使用reactive创建响应式对象
reactive用于创建对象类型的响应式数据:
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
message: 'Hello',
user: {
name: 'Alice',
age: 20
}
})
const increment = () => {
state.count++ // 直接修改reactive对象的属性
}
return {
state,
increment
}
}
}4.3 使用computed创建计算属性
computed用于创建计算属性:
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
// 创建计算属性
const double = computed(() => count.value * 2)
// 创建可写计算属性
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
}
})
return {
count,
double,
fullName
}
}
}4.4 使用watch监听数据变化
watch用于监听数据变化:
import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
// 监听单个ref
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
// 监听多个ref
watch([count1, count2], ([newCount1, newCount2], [oldCount1, oldCount2]) => {
console.log('Count1 or Count2 changed')
})
return {
count
}
}
}5. setup函数与生命周期钩子
在setup函数中,可以使用组合式API的生命周期钩子,它们的名称与选项式API的生命周期钩子类似,但前面加上了on前缀。
5.1 常用生命周期钩子
| 选项式API钩子 | 组合式API钩子 | 执行时机 |
|---|---|---|
| beforeCreate | 无(setup函数本身替代) | 组件实例创建之前 |
| created | 无(setup函数本身替代) | 组件实例创建之后 |
| beforeMount | onBeforeMount | 组件挂载之前 |
| mounted | onMounted | 组件挂载之后 |
| beforeUpdate | onBeforeUpdate | 组件更新之前 |
| updated | onUpdated | 组件更新之后 |
| beforeUnmount | onBeforeUnmount | 组件卸载之前 |
| unmounted | onUnmounted | 组件卸载之后 |
| errorCaptured | onErrorCaptured | 捕获子组件错误时 |
| renderTracked | onRenderTracked | 组件渲染时跟踪依赖 |
| renderTriggered | onRenderTriggered | 组件重新渲染时触发 |
5.2 使用示例
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
const count = ref(0)
let timer = null
onMounted(() => {
console.log('Component mounted')
// 组件挂载后启动定时器
timer = setInterval(() => {
count.value++
}, 1000)
})
onUpdated(() => {
console.log('Component updated')
})
onUnmounted(() => {
console.log('Component unmounted')
// 组件卸载前清理定时器
clearInterval(timer)
})
return {
count
}
}
}6. setup函数与选项式API的交互
虽然setup函数是组合式API的核心,但它可以与选项式API共存,用于渐进式迁移或在某些场景下结合使用。
6.1 共存规则
当setup函数与选项式API共存时,需要遵循以下规则:
- setup函数返回的属性会被合并到组件实例中
- 选项式API中的属性可以访问setup函数返回的属性
- setup函数中不能访问选项式API中的属性
- 生命周期钩子会同时执行,组合式API的钩子先执行
6.2 示例
import { ref } from 'vue'
export default {
data() {
return {
// 选项式API的data
message: 'Hello'
}
},
methods: {
// 选项式API的方法可以访问setup返回的属性
incrementFromOptions() {
this.count++
}
},
setup() {
// 组合式API的状态
const count = ref(0)
// 组合式API的方法
const incrementFromSetup = () => {
count.value++
}
return {
count,
incrementFromSetup
}
}
}6.3 渐进式迁移
对于现有的Vue 2组件,可以通过以下步骤渐进式迁移到组合式API:
- 保留原有的选项式API代码
- 添加setup函数,逐步将逻辑迁移到setup函数中
- 最后,将所有逻辑迁移到setup函数中,移除选项式API
7. setup函数的最佳实践
7.1 逻辑分组
将相关的逻辑组织在一起,提高代码的可读性和可维护性:
import { ref, computed, watch, onMounted } from 'vue'
export default {
setup() {
// 计数器逻辑
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
watch(count, (newVal) => {
console.log(`Count changed to: ${newVal}`)
})
// 用户信息逻辑
const user = ref(null)
const loading = ref(false)
const fetchUser = async () => {
loading.value = true
// 模拟API请求
user.value = await fetch('/api/user')
loading.value = false
}
onMounted(() => {
fetchUser()
})
return {
count,
double,
increment,
user,
loading
}
}
}7.2 合理使用组合式函数
将可复用的逻辑封装成组合式函数,提高代码复用性:
// useCounter.js
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
// 组件中使用
import { useCounter } from './useCounter'
export default {
setup() {
const { count, increment, decrement } = useCounter(10)
return {
count,
increment,
decrement
}
}
}7.3 避免在setup函数中使用this
setup函数中的this是undefined,不要尝试使用它来访问组件属性:
// 错误做法
export default {
setup() {
// 这里的this是undefined
console.log(this.message) // 报错
return {
// ...
}
}
}
// 正确做法
export default {
props: {
message: String
},
setup(props) {
console.log(props.message) // 正确
return {
// ...
}
}
}7.4 合理使用ref和reactive
根据数据类型选择合适的响应式API:
- 对于基本类型(string、number、boolean等),使用
ref - 对于对象类型,使用
reactive - 对于需要解构的响应式对象,使用
toRefs
import { ref, reactive, toRefs } from 'vue'
export default {
setup() {
// 基本类型使用ref
const count = ref(0)
// 对象类型使用reactive
const state = reactive({
name: 'Alice',
age: 20
})
// 解构响应式对象使用toRefs
const { name, age } = toRefs(state)
return {
count,
name,
age
}
}
}8. setup函数的性能优化
8.1 避免在setup函数中创建不必要的响应式数据
只对需要在模板中使用或需要监听变化的数据创建响应式:
// 好的做法
const count = ref(0) // 需要在模板中使用,创建响应式
const config = { /* ... */ } // 不需要响应式,直接创建普通对象
// 不好的做法
const config = reactive({ /* ... */ }) // 不需要响应式,却创建了响应式对象8.2 使用shallowRef和shallowReactive优化性能
对于深层嵌套的对象,如果只需要监听顶层属性的变化,可以使用shallowRef和shallowReactive:
import { shallowRef, shallowReactive } from 'vue'
export default {
setup() {
// 只监听list本身的变化,不监听list内部元素的变化
const list = shallowRef([{ id: 1, name: 'Item 1' }])
// 只监听user顶层属性的变化,不监听嵌套属性的变化
const user = shallowReactive({
name: 'Alice',
address: { city: 'Beijing', country: 'China' }
})
return {
list,
user
}
}
}8.3 使用markRaw避免不必要的响应式转换
对于不需要响应式的对象,可以使用markRaw标记,避免Vue对其进行响应式转换:
import { markRaw } from 'vue'
export default {
setup() {
// 标记为非响应式,提高性能
const nonReactiveObj = markRaw({
/* ... */
})
return {
nonReactiveObj
}
}
}9. setup函数与TypeScript
setup函数在TypeScript环境下有很好的支持,可以提供完善的类型推断和类型检查。
9.1 基本类型支持
import { ref, computed, defineComponent } from 'vue'
export default defineComponent({
setup() {
// 自动推断为Ref<number>
const count = ref(0)
// 手动指定类型
const message = ref<string>('Hello')
// 自动推断为ComputedRef<number>
const double = computed(() => count.value * 2)
return {
count,
message,
double
}
}
})9.2 Props类型支持
import { defineComponent } from 'vue'
interface Props {
message?: string
count: number
}
export default defineComponent({
props: {
message: {
type: String,
default: 'Hello'
},
count: {
type: Number,
required: true
}
} as const,
setup(props: Props) {
// 类型安全的props访问
console.log(props.message)
console.log(props.count)
return {
// ...
}
}
})9.3 Context类型支持
import { defineComponent } from 'vue'
export default defineComponent({
emits: ['custom-event'],
setup(props, context) {
// context具有正确的类型
context.emit('custom-event', 'data')
return {
// ...
}
}
})10. 总结
setup函数是Vue 3组合式API的核心入口,它具有以下特点:
- 在组件实例创建之前执行
- 接收props和context两个参数
- 返回对象或渲染函数
- this指向undefined
- 与响应式系统紧密结合
- 支持组合式API的生命周期钩子
- 可以与选项式API共存
- 提供完善的TypeScript支持
掌握setup函数的使用方法和最佳实践,是学习Vue 3组合式API的关键。通过合理使用setup函数,可以编写出更具可读性、可维护性和可复用性的Vue组件。
11. 实战练习
练习1:使用setup函数创建计数器组件
创建一个使用setup函数的计数器组件,包含以下功能:
- 显示当前计数
- 提供增加、减少、重置按钮
- 显示计数的双倍值
- 当计数变化时,在控制台打印日志
练习2:使用setup函数和组合式函数
创建一个组合式函数useMousePosition,用于跟踪鼠标位置,然后在组件中使用它:
- 跟踪鼠标的x和y坐标
- 在组件挂载时开始跟踪
- 在组件卸载时停止跟踪
- 在模板中显示鼠标位置
12. 扩展阅读
通过本集的学习,我们深入理解了Vue 3组合式API的核心入口setup函数。在下一集中,我们将学习生命周期钩子在setup中的使用,进一步掌握组合式API的生命周期管理。