1. Vue 3 setup 函数深度解析
作为一名长期使用 Vue 进行开发的前端工程师,我深刻体会到 Composition API 带来的变革。今天我想重点聊聊 setup 函数中两个最核心的参数 - props 和 context,这是每个 Vue 3 开发者都必须掌握的基础知识。
1.1 为什么需要 setup 函数
在 Vue 2 时代,我们主要通过 Options API 来组织组件代码。这种方式在小型组件中表现良好,但随着组件复杂度提升,相关逻辑会被分散到 data、methods、computed 等不同选项中,导致代码难以维护。Composition API 的 setup 函数正是为了解决这个问题而生,它让我们能够按逻辑关注点组织代码,而不是被选项分割。
2. props 参数详解
2.1 props 的核心特性
props 是 setup 函数的第一个参数,它包含了父组件传递给当前组件的所有属性。这里有几个关键点需要注意:
- 响应式特性:props 对象是响应式的,当父组件更新传递的属性时,子组件会自动接收到更新
- 只读限制:直接修改 props 会触发警告,这是 Vue 的单向数据流原则
- 显式声明:必须通过 defineProps 或 props 选项声明,否则无法正确接收
2.2 props 的使用实践
在实际开发中,我推荐使用 <script setup> 语法糖来声明 props:
vue复制<script setup>
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0,
validator: value => value >= 0
}
})
</script>
重要提示:在 TypeScript 项目中,可以使用基于类型的声明方式,这能获得更好的类型推断:
ts复制interface Props { title: string count?: number } const props = defineProps<Props>()
2.3 props 的解构陷阱
很多开发者喜欢解构 props 来简化代码,但这会导致响应式丢失:
javascript复制// ❌ 错误做法:解构会丢失响应性
const { title, count } = props
// ✅ 正确做法:使用 toRefs 保持响应性
import { toRefs } from 'vue'
const { title, count } = toRefs(props)
console.log(title.value) // 需要通过 .value 访问
如果只需要单个属性,也可以使用 toRef:
javascript复制import { toRef } from 'vue'
const title = toRef(props, 'title')
3. context 参数深度剖析
3.1 context 的组成结构
context 是 setup 的第二个参数,它包含三个重要属性:
javascript复制setup(props, context) {
const { attrs, slots, emit } = context
// 或者直接解构
setup(props, { attrs, slots, emit }) {
// ...
}
}
3.2 attrs 的特殊行为
attrs 包含所有未在 props 中声明的属性,包括:
- class 和 style
- 原生 HTML 属性
- 自定义事件监听器
javascript复制// 父组件
<MyComponent class="container" @click="handleClick" data-test="123" />
// 子组件
setup(props, { attrs }) {
console.log(attrs.class) // "container"
console.log(attrs.onClick) // handleClick 函数
console.log(attrs['data-test']) // "123"
}
注意:attrs 是非响应式的,但会自动更新。如果需要响应式访问,可以使用 useAttrs()
3.3 slots 的灵活用法
slots 允许我们更灵活地处理插槽内容:
javascript复制setup(props, { slots }) {
// 检查插槽是否存在
const hasFooter = !!slots.footer
// 渲染作用域插槽
return () => (
<div>
{slots.header?.({ title: props.title })}
{slots.default?.()}
{hasFooter && <div class="footer">{slots.footer()}</div>}
</div>
)
}
在 <script setup> 中,推荐使用 useSlots():
vue复制<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>
3.4 emit 的最佳实践
emit 用于向父组件触发事件,应该始终使用 defineEmits 进行声明:
vue复制<script setup>
const emit = defineEmits(['update', 'delete'])
function handleClick() {
emit('update', { id: 1, value: 'new' })
}
</script>
在 TypeScript 中,可以定义完整的事件签名:
ts复制const emit = defineEmits<{
(e: 'update', payload: { id: number; value: string }): void
(e: 'delete', id: number): void
}>()
4. 为什么 setup 中没有 this
4.1 设计哲学解析
Vue 3 移除 setup 中的 this 访问是经过深思熟虑的设计决策:
- 执行时机问题:setup 在 beforeCreate 之前运行,此时组件实例尚未创建
- 函数式编程:减少对 this 的依赖,避免上下文绑定问题
- TypeScript 支持:this 的类型推断复杂,函数参数更易类型化
- 代码组织:鼓励将逻辑提取到独立的组合函数中
4.2 常见 this 替代方案
| Vue 2 (this) | Vue 3 替代方案 |
|---|---|
| this.$props | defineProps() |
| this.$data | ref/reactive |
| this.$refs | template refs |
| this.$router | useRouter() |
| this.$store | useStore() |
4.3 getCurrentInstance 的谨慎使用
虽然可以通过 getCurrentInstance 获取组件实例,但官方不推荐:
javascript复制import { getCurrentInstance } from 'vue'
setup() {
const instance = getCurrentInstance()
// 不推荐操作内部实例
instance.ctx.someInternalProperty = '...'
}
应该仅在绝对必要时使用,比如:
- 开发高阶组件
- 编写自定义开发工具
- 处理极端边界情况
5. 实战经验与避坑指南
5.1 性能优化技巧
-
避免不必要的响应式转换:
javascript复制// 如果只需要读取一次值 const staticValue = props.initialValue -
合理使用 watchEffect:
javascript复制watchEffect(() => { // 会自动追踪 props.id 的依赖 fetchData(props.id) }) -
事件节流处理:
javascript复制import { throttle } from 'lodash-es' const throttledEmit = throttle(emit, 100)
5.2 常见问题排查
-
props 未更新:
- 检查父组件是否使用 reactive 包装对象
- 确保没有直接修改 props
-
emit 事件未触发:
- 检查事件名是否完全匹配(大小写敏感)
- 确认父组件是否正确监听
-
插槽内容不显示:
- 使用
!!slots.default检查插槽是否存在 - 确保没有 v-if 阻止渲染
- 使用
5.3 组合式函数实践
将 setup 逻辑提取到组合式函数中:
javascript复制// useCounter.js
export function useCounter(initialValue = 0, emit) {
const count = ref(initialValue)
const increment = () => {
count.value++
emit?.('update', count.value)
}
return { count, increment }
}
// 组件中使用
import { useCounter } from './useCounter'
setup(props, { emit }) {
const { count, increment } = useCounter(props.initialCount, emit)
return { count, increment }
}
6. TypeScript 深度集成
6.1 类型安全 props
typescript复制interface Props {
title: string
size?: 'small' | 'medium' | 'large'
items?: Array<{ id: number; text: string }>
}
const props = defineProps<Props>()
6.2 严格类型 emit
typescript复制const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'delete', id: number, confirm: boolean): void
}>()
6.3 组件实例类型
如果需要访问组件实例,应该使用 InstanceType:
typescript复制import MyComponent from './MyComponent.vue'
const instance = ref<InstanceType<typeof MyComponent>>()
7. 生态系统集成
7.1 与 Vue Router 配合
javascript复制import { useRoute, useRouter } from 'vue-router'
setup() {
const route = useRoute()
const router = useRouter()
function navigate() {
router.push('/new-route')
}
return { navigate }
}
7.2 与 Pinia 状态管理
javascript复制import { useStore } from '@/stores/counter'
setup() {
const store = useStore()
function increment() {
store.increment()
}
return { increment }
}
8. 高级模式与技巧
8.1 动态组件处理
javascript复制import { markRaw } from 'vue'
setup() {
const components = {
home: markRaw(defineAsyncComponent(() => import('./Home.vue'))),
about: markRaw(defineAsyncComponent(() => import('./About.vue')))
}
const currentComponent = ref('home')
return { components, currentComponent }
}
8.2 依赖注入模式
javascript复制// 父组件
import { provide } from 'vue'
setup() {
provide('theme', 'dark')
}
// 子组件
import { inject } from 'vue'
setup() {
const theme = inject('theme', 'light') // 默认值 'light'
return { theme }
}
8.3 模板引用处理
vue复制<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="inputRef" />
</template>
经过多年 Vue 3 项目实践,我发现 setup 函数的设计确实大幅提升了代码的可维护性和复用性。刚开始可能需要适应没有 this 的编程模式,但一旦熟悉后,你会发现这种显式的依赖声明让组件逻辑变得更加清晰和可预测。