何何____头像
关注

Vue 3 setup 详解

一、什么是 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/emitsetup(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 怎么选

维度refreactive
适用类型基本类型 + 对象(万能)只能对象/数组
访问方式要 .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 APIsetup 中写法
beforeCreate不需要(setup 代替)
created不需要(setup 代替)
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
activatedonActivated
deactivatedonDeactivated
errorCapturedonErrorCaptured
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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--