一、什么是 setup
1.1 定义
setup 是 Vue 3 引入的组合式 API(Composition API)的入口。它在组件创建之前执行(在 beforeCreate 之前),是整个组件逻辑的"起跑线"。
关键点:setup 是一个函数,你把响应式数据、计算属性、方法、生命周期钩子全都写在这个函数内部,然后通过 return 暴露给模板使用。
1.2 生活类比
把组件想象成一个"公司":
- Options API(Vue 2):公司分好几个部门(data 部、methods 部、computed 部、mounted 部),各部在不同楼层上班,要协作得上下跑
- Composition API + setup:一个大办公室,所有人都坐一起,数据、方法、计算、生命周期挨着写,想拿谁直接转身
setup 就是这个大办公室。
1.3 为什么要用 setup
| 痛点 | setup 的解法 |
|---|---|
| 一个功能的代码散落各处 | 同一功能的 data/methods/computed 写一起 |
| mixin 容易冲突、难追踪 | 用组合函数(useXxx)替代,来源清晰 |
| 类型推导差(TypeScript) | 组合式 API 天然适合类型推导 |
| 逻辑复用难 | 把 setup 里的代码抽成函数,随便复用 |
二、两种写法:setup() 函数 vs <script setup>
Vue 3 提供了两种使用组合式 API 的方式,写法不同但能力基本一致。
2.1 写法一:setup() 函数(Options API 中的 setup)
在 export default 里写一个 setup 方法,返回值暴露给模板。
<script>
import { ref } from "vue";
export default {
setup() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment }; // ← 必须 return 模板才能用
}
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
2.2 写法二:<script setup> 语法糖(推荐)
在 <script> 标签上加 setup 属性,所有顶层变量自动暴露给模板,不用 return,也不用 export default。
<script setup>
import { ref } from "vue";
const count = ref(0);
const increment = () => count.value++;
// 不用 return!不用 export default!
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
2.3 对比
| 特性 | setup() 函数 | <script setup> |
|---|---|---|
需不需要 return | 必须 return | 自动暴露 |
需不需要 export default | 需要 | 不需要 |
| 组件注册 | components: {} 注册 | 导入即可用 |
| props/emit | setup(props, context) | defineProps/defineEmits |
| 代码量 | 多(return、注册) | 少 |
| 推荐度 | 兼容老代码时用 | 推荐,更简洁 |
结论:日常开发直接用 <script setup>,简洁且是官方主推写法。
三、响应式数据:ref & reactive
setup 里声明的普通变量不是响应式的,改了页面不更新。要让数据变响应式,必须用 ref 或 reactive。
3.1 ref:基本类型和对象都能用
ref 接收一个值,返回一个带 .value 的响应式对象。
import { ref } from "vue";
const count = ref(0); // 基本类型
count.value++; // ← 取值/赋值都要 .value
const user = ref({ name: "张三" }); // 对象也行
user.value.name = "李四"; // ← 对象要 .value.name
模板里不用写 .value,Vue 自动解包:
<p>{{ count }}</p> <!-- 不用 count.value -->
3.2 reactive:专门给对象用
reactive 只能接对象/数组,返回的响应式对象不用 .value,直接访问属性。
import { reactive } from "vue";
const user = reactive({ name: "张三", age: 18 });
user.age++; // ← 直接 user.age,不用 .value
const list = reactive([1, 2, 3]);
list.push(4); // ← 数组也直接操作
3.3 ref vs reactive 怎么选
| 维度 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型 + 对象(万能) | 只能对象/数组 |
| 访问方式 | 要 .value | 不要 .value |
| 解构赋值 | 解构后仍响应式(因为是对象) | 解构后丢失响应式 |
| 整体替换 | ref.value = xxx 可以 | 不能整体替换(会丢响应) |
| 推荐度 | 更通用,优先用 | 适合嵌套深的对象 |
记忆口诀:基本类型用 ref,复杂对象用 reactive;能 ref 就 ref,省事。
3.4 注解
1.reactive 解构会丢响应式
const user = reactive({ name: "张三", age: 18 });
const { age } = user; // ← age 变成普通数字,改了不更新
age++; // 不响应
要保持响应式,用 toRefs:
import { toRefs } from "vue";
const { age } = toRefs(user); // ← age 是 ref,仍是响应式
age.value++; // 响应
2.ref 在模板里自动解包,但只解一层
const obj = ref({ count: ref(0) });
// 模板里 obj.count 还是 ref,需要 obj.count.value
避免嵌套 ref,保持数据结构扁平。
四、computed & watch
4.1 computed:计算属性
import { ref, computed } from "vue";
const firstName = ref("张");
const lastName = ref("三");
const fullName = computed(() => firstName.value + lastName.value);
// 模板里直接用 {{ fullName }},自动解包
也支持 get/set:
const fullName = computed({
get: () => firstName.value + lastName.value,
set: (val) => {
firstName.value = val[0];
lastName.value = val.slice(1);
}
});
4.2 watch:侦听器
watch 可以监听响应式数据变化,执行副作用(请求、日志等)。
import { ref, watch } from "vue";
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`从 ${oldVal} 变成 ${newVal}`);
});
监听多个源:
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
// ...
});
监听对象的某个属性,要传 getter:
watch(() => user.age, (newVal, oldVal) => {
console.log("age 变了");
});
watchEffect:自动收集依赖,不用指定监听谁,里面用到的响应式数据变了就触发。
import { watchEffect } from "vue";
watchEffect(() => {
console.log(count.value); // ← count 变了就重新执行
});
4.3 注解
1.watch 默认是懒执行的
watch 第一次不会执行,只有数据变化才触发。想立即执行,加 immediate: true:
watch(count, fn, { immediate: true }); // ← 立刻执行一次
2.watch 监听对象默认不深度遍历
watch(user, fn) 只在 user 整体被替换时触发,改 user.age 不会触发。要深度监听加 deep: true,或直接监听 () => user.age。
五、生命周期钩子:全加 on
setup 里没有 created/beforeCreate(setup 本身就在这之前执行),其他钩子都加个 on 前缀。
| Options API | setup 中写法 |
|---|---|
| beforeCreate | 不需要(setup 代替) |
| created | 不需要(setup 代替) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| activated | onActivated |
| deactivated | onDeactivated |
| errorCaptured | onErrorCaptured |
import { onMounted, onBeforeUnmount } from "vue";
onMounted(() => {
console.log("DOM 已挂载,可以操作 DOM 了");
});
onBeforeUnmount(() => {
console.log("要卸载了,清理定时器/监听");
});
注意:钩子必须在 setup 同步执行时调用,不能放在异步回调里(如 setTimeout 里调 onMounted 无效)。
六、props & emit
6.1 setup() 函数写法
setup 接收两个参数:props 和 context。
export default {
props: {
title: String,
count: Number
},
setup(props, context) {
console.log(props.title); // ← 访问 props
context.emit("change", props.count + 1); // ← 触发事件
console.log(context.attrs); // 透传属性
console.log(context.slots); // 插槽
console.log(context.expose); // 暴露给父组件
return {};
}
}
注意:props 是响应式的,但不能解构(解构会丢响应式):
const { title } = props; // ❌ 解构后 title 不是响应式
const title = props.title; // ❌ 同上
// 正确:直接用 props.title,或用 toRefs
import { toRefs } from "vue";
const { title } = toRefs(props); // ✅ title 是 ref
6.2 <script setup> 写法:defineProps & defineEmits
<script setup> 用编译器宏 defineProps 和 defineEmits,不需要 import。
<script setup>
const props = defineProps({
title: String,
count: { type: Number, default: 0 }
});
const emit = defineEmits(["change", "update"]);
const handleClick = () => {
emit("change", props.count + 1);
};
</script>
<template>
<div>{{ title }}</div>
<button @click="handleClick">+1</button>
</template>
TypeScript 写法(泛型声明,更推荐):
<script setup lang="ts">
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0
});
const emit = defineEmits<{
(e: "change", value: number): void;
(e: "update"): void;
}>();
</script>
6.3 defineExpose:暴露给父组件
子组件默认不向父组件暴露任何东西,父组件通过 ref 拿到的只是组件实例。要暴露数据/方法,用 defineExpose。
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from "vue";
const count = ref(0);
const reset = () => { count.value = 0; };
defineExpose({ count, reset }); // ← 暴露给父组件
</script>
<!-- 父组件 -->
<script setup>
import { ref, onMounted } from "vue";
import Child from "./Child.vue";
const childRef = ref(null);
onMounted(() => {
console.log(childRef.value.count); // 0
childRef.value.reset(); // 调子组件方法
});
</script>
<template>
<Child ref="childRef" />
</template>
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2501_93772268/article/details/167221658



