Vue 3 Composition API 深入


前言

Composition API 是 Vue 3 组织组件逻辑的核心方式。前几篇已涉及 refcomputedComposables 等,本篇从语法与机制层面深入,讲清楚:

  • setup()<script setup> 的关系
  • 编译器宏的使用与限制
  • setup 中没有 this 的原因与替代方式
  • reactive 解构与组件实例访问

一、setup 函数

1.1 执行时机

创建组件实例
    ↓
初始化 props
    ↓
setup(props, context)  ← 在此执行
    ↓
beforeMount → mounted → ...
  • beforeCreate / created 之前执行,等价替代这两个钩子
  • 此时实例尚未完全创建,没有 this

1.2 基本写法

<script>
import { ref, computed, onMounted } from 'vue'

export default {
  props: { title: String },
  setup(props, { emit, attrs, slots, expose }) {
    const count = ref(0)
    const double = computed(() => count.value * 2)

    const increment = () => {
      count.value++
      emit('update', count.value)
    }

    onMounted(() => {
      console.log('mounted', props.title)
    })

    // 必须 return 才能在模板中使用
    return {
      count,
      double,
      increment
    }
  }
}
</script>

<template>
  <p>{{ count }} × 2 = {{ double }}</p>
  <button @click="increment">+1</button>
</template>

1.3 context 参数

属性含义
attrs非 props 的属性(class、style、id 等)
slots插槽内容
emit触发事件
expose显式暴露给父组件 ref 的内容
setup(props, { emit, expose }) {
  const validate = () => true
  expose({ validate })  // 等价于 defineExpose
  return { /* ... */ }
}

二、script setup 语法糖

2.1 是什么

<script setup>setup()编译时语法糖:顶层变量、函数、import 自动暴露给模板,无需 return

<!-- 等价于上面的 setup + return -->
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>

<template>
  <p>{{ double }}</p>
  <button @click="increment">+1</button>
</template>

2.2 对比

对比项setup()<script setup>
return必须手动 return顶层自动暴露
组件名export default { name }defineOptions({ name })
props/emit选项或 setup 参数defineProps / defineEmits
默认导出需要不需要,<script setup> 即组件
推荐度老项目/特殊场景新项目首选

2.3 与普通 script 共存

<script>
// 普通 script:仅执行一次,不能访问 setup 变量
export default {
  inheritAttrs: false
}
</script>

<script setup>
const count = ref(0)
</script>

三、编译器宏

编译器宏在 <script setup>无需 import,编译阶段被处理,不能放在条件语句或函数内。

3.1 defineProps

<script setup>
// 运行时声明
const props = defineProps(['title', 'count'])

// 类型 + 默认值
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 }
})

// TypeScript
const props = defineProps<{
  title: string
  count?: number
}>()
</script>

3.2 defineEmits

<script setup>
const emit = defineEmits(['update', 'close'])

// 类型
const emit = defineEmits<{
  update: [value: number]
  close: []
}>()

const submit = () => emit('update', 1)
</script>

3.3 defineExpose

<script setup>
const formRef = ref(null)
const validate = () => { /* ... */ }

defineExpose({ validate, formRef })
</script>

父组件 childRef.value.validate() 只能访问 expose 的内容。

3.4 defineOptions(Vue 3.3+)

<script setup>
defineOptions({
  name: 'UserList',
  inheritAttrs: false
})
</script>

替代在普通 <script> 里写组件选项。

3.5 defineModel(Vue 3.4+)

<script setup>
// 等价于 modelValue + update:modelValue
const title = defineModel('title', { type: String, default: '' })
</script>

<template>
  <input v-model="title" />
</template>

简化 v-model 多个绑定名的写法。


四、setup 中没有 this

4.1 为什么

setup 执行时组件实例尚未创建,不存在 this。这是刻意设计,避免 Options API 中 this 指向混乱。

setup() {
  console.log(this)  // undefined(严格模式下)
}

4.2 如何替代

Options APIComposition API
this.xxx 数据ref / reactive
this.$propsdefineProps / setup(props)
this.$emitdefineEmits / emit
this.$attrsuseAttrs()
this.$slotsuseSlots()
this.$refsref() 模板引用
组件实例getCurrentInstance()(慎用)

4.3 getCurrentInstance

<script setup>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()
// 开发调试可用,不推荐业务逻辑依赖
console.log(instance?.proxy)  // 类似 this 的代理
</script>

注意:仅用于插件、底层库;业务代码应使用 props、emit、composables,避免依赖内部实例。


五、useAttrs 与 useSlots

5.1 useAttrs

<script setup>
import { useAttrs } from 'vue'

defineProps(['label'])
const attrs = useAttrs()  // 除 props 外的属性(class、data-* 等)
</script>

<template>
  <label>
    {{ label }}
    <input v-bind="attrs" />
  </label>
</template>

配合 inheritAttrs: false 手动绑定 attrs 到内部元素。

5.2 useSlots

<script setup>
import { useSlots } from 'vue'

const slots = useSlots()

onMounted(() => {
  console.log(slots.header?.())
})
</script>

<template>
  <header v-if="slots.header">
    <slot name="header" />
  </header>
</template>

六、reactive 解构失去响应性

6.1 问题

const state = reactive({ count: 0, name: 'Vue' })

const { count, name } = state
count++  // ❌ 不是响应式的,只是普通数字

解构得到的是当前值的拷贝,与 reactive 对象断开连接。

6.2 toRefs 解决

import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)

count.value++  // ✅ 响应式

toRefs 把每个属性转成 ref,解构后仍保持响应性。

6.3 toRef 单个属性

const count = toRef(state, 'count')
// 或
const count = toRef(() => state.count)

6.4 props 解构

<script setup>
const props = defineProps(['title'])

// ❌ 失去响应性
const { title } = props

// ✅ 保持响应性
const { title } = toRefs(props)
// 或
const title = toRef(() => props.title)
// 或 Vue 3.5+ 响应式 props 解构(编译器支持)
</script>

七、逻辑组织方式

7.1 按功能分组

<script setup>
// ===== 搜索逻辑 =====
const keyword = ref('')
const results = ref([])
const search = async () => { /* ... */ }

// ===== 分页逻辑 =====
const page = ref(1)
const pageSize = ref(10)
const changePage = (p) => { page.value = p; search() }

// ===== 生命周期 =====
onMounted(search)
</script>

复杂组件按业务关注点组织,而非 Options API 的 data/methods 分散。

7.2 提取 Composable

// composables/useSearch.js
export function useSearch(fetchApi) {
  const keyword = ref('')
  const results = ref([])
  const search = async () => { /* ... */ }
  return { keyword, results, search }
}

详见《Mixins 与 Composables》篇。


八、与 Options API 混用

export default {
  data() {
    return { legacy: 1 }
  },
  setup() {
    const modern = ref(2)
    return { modern }
  }
}

可以共存,但不推荐同一组件混用两种风格,维护成本高。新代码统一用 Composition API + <script setup>


九、面试聚焦

9.1 setup 中没有 this

实例未创建,用 props、ref、emit 等显式 API 替代 this

9.2 script setup 如何获取组件实例?

getCurrentInstance(),仅建议底层场景;业务用 props/emit/composables。

9.3 defineProps 为什么要编译器宏?

编译期提取 props 定义,生成运行时校验,且无需 import、性能更好;不能在运行时条件分支中使用。

9.4 reactive 解构为什么失去响应性?

解构取的是当前值快照;用 toRefs / toRef 转为 ref 保持引用。


十、易混淆点

  1. setup 替代 beforeCreate/created:不是替代 mounted。
  2. 编译器宏不能 import:defineProps 等是编译指令,非运行时函数。
  3. script setup 无 return:顶层绑定即模板可用。
  4. getCurrentInstance 仅 setup 同步阶段可靠:异步回调中可能为 null。
  5. ref 解构也失去响应性:需 storeToRefs(Pinia)或保持 xxx.value

十一、思考与练习

1. setup 和 script setup 的关系?

解析:script setup 是 setup 的语法糖,编译后等价于 setup 函数 + return 顶层绑定。

2. 为什么 setup 里没有 this?

解析:执行时组件实例尚未创建;鼓励显式 API,避免 this 指向问题。

3. defineExpose 的作用?

解析:script setup 默认封闭,defineExpose 指定父组件 ref 可访问的方法/属性。

4. reactive 解构如何解决响应性?

解析:使用 toRefs 将各属性转为 ref,解构后通过 .value 保持响应。

5. defineProps 能在 if 里调用吗?

解析:不能,编译器宏必须在 script setup 顶层同步调用。


总结

  • setup:Composition API 入口,无 this,需 return(或 script setup 自动暴露)
  • script setup:推荐写法,顶层变量即模板可用
  • 编译器宏:defineProps / defineEmits / defineExpose / defineOptions / defineModel
  • 实例访问:业务用 props、emit、useAttrs;getCurrentInstance 慎用
  • 解构:reactive、props 解构需 toRefs / toRef 保持响应性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值