props 是响应式的只读对象,需通过defineProps声明,可使用toRefs保持解构后的响应性。
context 包含三个非响应式属性:attrs(未声明的属性)、slots(插槽内容)和emit(触发事件)。
setup 中无法访问 this,context 提供了 this 上面三个最常用的属性。
在<script setup>中推荐使用useAttrs()、useSlots()和defineEmits()。
注意事项包括setup执行时机、响应式丢失风险及TypeScript支持。
最佳实践建议优先使用<script setup>语法糖,配合类型声明和emit事件通信,充分利用Vue3响应式系统。
在 Vue3 的 Composition API 中,setup 函数接收两个重要参数:props 和 context。
1. props 参数
props 是响应式的对象,包含组件接收的所有 prop。
特性:
-
响应式 :当父组件传递的 prop 变化时,
props会自动更新 -
只读:不能在子组件中直接修改(会触发警告)
-
需要显式声明 :必须通过
defineProps或props选项声明
基本使用:
vue
javascript
<script setup>
import { toRefs, watch } from 'vue'
// 声明 props
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
// 直接使用
console.log(props.title)
// 监听 props 变化
watch(() => props.count, (newVal) => {
console.log('count changed:', newVal)
})
// 解构 props(会失去响应性)
const { title } = props // ❌ 非响应式
// 保持响应性的解构
const { title, count } = toRefs(props) // ✅ 响应式
</script>
2. context 参数
context 是一个普通对象(非响应式),包含三个组件属性。
context 包含三个属性:
a) attrs
vue
javascript
<script setup>
import { useAttrs } from 'vue'
// 方式一:通过 setup 参数
setup(props, context) {
console.log(context.attrs) // 所有未在 props 中声明的 attribute
}
// 方式二:在 <script setup> 中使用 useAttrs()
const attrs = useAttrs()
console.log(attrs.class) // 获取 class 属性
console.log(attrs.onClick) // 获取事件监听器
</script>
-
包含所有未在 props 中声明的 attribute
-
包括
class、style、事件监听器等 -
非响应式,但会自动更新
b) slots
vue
javascript
<script setup>
import { useSlots } from 'vue'
// 方式一:通过 setup 参数
setup(props, context) {
// 检查插槽是否存在
if (context.slots.default) {
// 渲染插槽内容
return () => context.slots.default()
}
}
// 方式二:在 <script setup> 中使用 useSlots()
const slots = useSlots()
console.log(slots.default) // 默认插槽
console.log(slots.header) // 具名插槽
</script>
-
包含所有插槽内容的函数
-
用于渲染插槽内容或检查插槽是否存在
c) emit
vue
javascript
<script setup>
import { defineEmits } from 'vue'
// 方式一:通过 setup 参数
setup(props, context) {
const handleClick = () => {
context.emit('update', newValue)
}
}
// 方式二:在 <script setup> 中使用 defineEmits()
const emit = defineEmits(['update', 'delete'])
const handleClick = () => {
emit('update', { id: 1, value: 'new' })
emit('delete', 1)
}
</script>
-
用于触发自定义事件
-
推荐使用
defineEmits进行声明
3. 完整示例
选项式 API 写法:
vue
javascript
<script>
import { toRefs, watch } from 'vue'
export default {
props: ['title', 'count'],
emits: ['update-count'],
setup(props, context) {
// 使用 props
const { title, count } = toRefs(props)
// 使用 attrs
console.log('所有 attributes:', context.attrs)
// 使用 slots
const hasHeaderSlot = !!context.slots.header
// 使用 emit
const increment = () => {
context.emit('update-count', count.value + 1)
}
// 监听 props 变化
watch(count, (newVal) => {
console.log('Count updated:', newVal)
})
return {
title,
count,
increment,
hasHeaderSlot
}
}
}
</script>
<script setup> 语法糖写法:
vue
javascript
<script setup>
import { toRefs, watch, useAttrs, useSlots } from 'vue'
// 声明 props
const props = defineProps({
title: String,
count: Number
})
// 声明 emits
const emit = defineEmits(['update-count'])
// 使用 attrs 和 slots
const attrs = useAttrs()
const slots = useSlots()
// 响应式解构 props
const { title, count } = toRefs(props)
// 检查插槽
const hasHeaderSlot = !!slots.header
// 方法
const increment = () => {
emit('update-count', count.value + 1)
}
// 监听器
watch(count, (newVal) => {
console.log('Count updated:', newVal)
})
</script>
<template>
<div :class="attrs.class">
<slot name="header" v-if="hasHeaderSlot" />
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<slot />
</div>
</template>
4. 注意事项
-
setup 执行时机 :在
beforeCreate之前执行,此时无法访问this -
响应式丢失 :直接解构 props 会丢失响应性,使用
toRefs或toRef -
attrs 非响应式:但会自动更新,无需担心
-
TypeScript 支持:
vue
javascript
<script setup lang="ts">
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update', value: number): void
}>()
</script>
5. 最佳实践
-
优先使用
<script setup>语法糖 -
使用
defineProps和defineEmits进行声明 -
需要响应式解构时使用
toRefs -
避免直接修改 props,使用 emit 触发事件
-
使用 TypeScript 增强类型安全
通过合理使用这两个参数,可以更好地组织组件逻辑,并充分利用 Vue3 的响应式系统。
setup 中无法访问 this,context 提供了 this 上面三个最常用的属性
这正是 Vue3 Composition API 的核心设计理念之一。
为什么 setup 中不能访问 this?
-
执行时机问题:
-
setup在组件实例创建之前 执行(在beforeCreate之前) -
此时组件实例(this)还没有被创建,所以无法访问
-
-
函数式编程风格:
-
Composition API 鼓励函数式编程,减少对
this的依赖 -
避免
this指向问题,特别是在高阶函数、回调函数中
-
-
更好的 TypeScript 支持:
-
this在 TypeScript 中类型推导复杂 -
函数参数和返回值更容易进行类型声明
-
context 确实提供了 this 的三个核心功能
| this 上的属性 | context 对应属性 | 用途 |
|---|---|---|
this.$attrs |
context.attrs |
获取未声明的 attribute |
this.$slots |
context.slots |
访问插槽内容 |
this.$emit |
context.emit |
触发自定义事件 |
this 的其他功能在 setup 中如何获取?
vue
javascript
<script setup>
import {
getCurrentInstance,
useAttrs,
useSlots
} from 'vue'
// 替代 this.$attrs
const attrs = useAttrs() // 等价于 this.$attrs
// 替代 this.$slots
const slots = useSlots() // 等价于 this.$slots
// 如果需要访问组件实例(谨慎使用)
const instance = getCurrentInstance()
// this.$props → 通过 defineProps
const props = defineProps(['title'])
// this.$data → 使用 ref/reactive
const data = reactive({ count: 0 })
// this.$watch → 使用 watch API
watch(() => props.title, (newVal) => {
console.log(newVal)
})
// this.$nextTick → 使用 nextTick API
import { nextTick } from 'vue'
nextTick(() => {
console.log('DOM updated')
})
// this.$root → 不推荐,考虑使用 provide/inject
</script>
详细的对比示例
Vue2 Options API(使用 this)
javascript
export default {
props: ['title'],
data() {
return { count: 0 }
},
mounted() {
// 访问各种属性
console.log(this.title) // props
console.log(this.count) // data
console.log(this.$attrs) // attrs
console.log(this.$slots) // slots
this.$emit('update', 1) // emit
this.$watch('title', fn) // watch
this.$nextTick(() => {}) // nextTick
}
}
Vue3 Composition API(不使用 this)
vue
javascript
<script setup>
import {
ref,
watch,
nextTick,
getCurrentInstance,
onMounted
} from 'vue'
// props(替代 this.props)
const props = defineProps(['title'])
// data(替代 this.data)
const count = ref(0)
// attrs(替代 this.$attrs)
const attrs = useAttrs()
// slots(替代 this.$slots)
const slots = useSlots()
// emit(替代 this.$emit)
const emit = defineEmits(['update'])
// watch(替代 this.$watch)
watch(() => props.title, (newVal) => {
console.log(newVal)
})
// 生命周期
onMounted(() => {
console.log(props.title) // props
console.log(count.value) // data
console.log(attrs) // attrs
console.log(slots) // slots
emit('update', 1) // emit
// nextTick(替代 this.$nextTick)
nextTick(() => {
console.log('DOM updated')
})
// 如果需要组件实例(慎用!)
const instance = getCurrentInstance()
console.log(instance)
})
</script>
为什么不推荐在 setup 中使用 getCurrentInstance()?
虽然可以通过 getCurrentInstance() 获取组件实例,但官方不推荐这样做:
-
破坏封装性:直接操作实例可能导致不可预测的行为
-
类型安全 :返回的实例类型是
ComponentInternalInstance,内部 API 不稳定 -
测试困难:依赖实例的方法难以进行单元测试
-
设计原则:违背了 Composition API 的声明式设计理念
javascript
javascript
// ❌ 不推荐的做法
const instance = getCurrentInstance()
instance.proxy.$forceUpdate() // 强制更新,应该避免
// ✅ 推荐的做法:使用响应式数据驱动
const state = reactive({ count: 0 })
state.count++ // 自动触发更新
特殊场景:在普通函数中访问组件上下文
如果你需要在 setup 外部的工具函数中访问组件功能,可以传递需要的部分:
javascript
// utils.js
// ❌ 不好的做法:尝试在工具函数中访问 this
export function badUtil() {
// 这里无法访问 this
}
// ✅ 好的做法:显式传递依赖
export function goodUtil(emit, props) {
// 使用传入的 emit 和 props
emit('event', props.value)
}
// 在组件中使用
import { goodUtil } from './utils'
const props = defineProps(['value'])
const emit = defineEmits(['event'])
// 显式传递依赖
goodUtil(emit, props)
总结
-
setup中无法访问this是设计选择,不是限制 -
context提供了attrs、slots、emit这三个最常用的功能 -
其他功能通过独立的 API 提供(ref、watch、nextTick 等)
-
避免使用
getCurrentInstance(),优先使用声明式 API -
这种设计带来更好的组合性、类型安全和测试便利
这种设计让组件逻辑更加清晰、可测试,也更容易提取和重用逻辑代码。