一、Options API 与 Composition API 区别

1. Options API(Vue2 主流)

Vue2 采用配置式 API,代码按照 datamethodscomputedwatch 拆分。
缺点:同一个业务逻辑的代码被分散在不同配置项中,项目复杂后难以维护、逻辑复用困难。

<script>
export default {
  data() {
    return { name: "张三", age: 18 }
  },
  methods: {
    changeName() { this.name = "李四" }
  }
}
</script>

2. Composition API(Vue3 主流)

Vue3 推出组合式API,以函数形式组织代码,相同功能的代码写在一起,逻辑聚合、复用灵活,是 Vue3 核心设计思想。

二、setup 入口函数

setup 是 Composition API 的执行入口,所有组合式 API 都要在 setup 中使用。

1. 基础特性

  1. setupbeforeCreate 生命周期之前执行,执行时 thisundefined不能使用 this
  2. setup 返回的对象属性/方法,模板可直接使用。
  3. Vue2 的 Options API 可以访问 setup 中的内容,setup 无法访问 Options API。
  4. 命名冲突时,setup 优先级更高。

2. 基础用法(非语法糖)

<template>
  <div>
    <h2>姓名:{{ name }}</h2>
    <button @click="changeName">修改姓名</button>
  </div>
</template>

<script lang="ts">
export default {
  name: "Person",
  setup() {
    // 普通变量(非响应式)
    let name = "张三";

    // 方法
    function changeName() {
      name = "李四";
      console.log(name);
    }

    // 必须返回,模板才能使用
    return { name, changeName };
  },
};
</script>

注意:普通 let/const 定义的数据不是响应式,修改后页面不会更新。

3. <script setup> 语法糖(重点推荐)

Vue3 官方推荐语法糖,简化 setup 写法,无需手动 return,代码更简洁。

基础使用
<template>
  <h2>姓名:{{ name }}</h2>
  <button @click="changeName">修改姓名</button>
</template>

<!-- 语法糖写法,整个标签等价于 setup 函数 -->
<script lang="ts" setup>
console.log(this); // undefined
let name = "张三";

function changeName() {
  name = "李四";
}
</script>
扩展:自动补充组件名插件

原生语法糖无法直接写组件名,可借助插件简化:

  1. 安装依赖
npm i vite-plugin-vue-setup-extend -D
  1. 配置 vite.config.ts
import { defineConfig } from "vite";
import VueSetupExtend from "vite-plugin-vue-setup-extend";

export default defineConfig({
  plugins: [VueSetupExtend()],
});
  1. 直接在标签上定义组件名
<script lang="ts" setup name="Person">
  // 业务代码
</script>

三、响应式 API:ref 与 reactive

响应式是 Vue 核心,Vue3 使用 Proxy 替代 Object.defineProperty 实现响应式,解决了 Vue2 数组/对象更新的缺陷。

1. ref 基本类型响应式

作用

定义基本数据类型(字符串、数字、布尔)响应式,也可接收对象/数组。

语法

const 变量 = ref(初始值)

特点
  1. 返回 RefImpl 实例对象,JS 中操作必须加 .value,模板中直接使用。
  2. 支持基本类型、对象、数组。
<template>
  <div>
    <h2>姓名:{{ name }}</h2>
    <h2>年龄:{{ age }}</h2>
    <button @click="changeAge">年龄+1</button>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

// 定义响应式数据
let name = ref("张三");
let age = ref(18);

function changeAge() {
  // JS 内部必须使用 .value
  age.value += 1;
}
</script>

2. reactive 对象类型响应式

作用

专门定义对象、数组类型响应式数据,深度响应式(嵌套属性也可监听)。

语法

const 响应式对象 = reactive(原始对象)

特点
  1. 返回 Proxy 代理对象,无需 .value,直接操作属性。
  2. 天然支持深层嵌套响应。
  3. 不要直接重新赋值整个对象,会丢失响应式(可用 Object.assign 批量更新)。
<template>
  <div>
    <h2>汽车:{{ car.brand }},价格:{{ car.price }}万</h2>
    <button @click="changePrice">涨价</button>
    <ul>
      <li v-for="item in gameList" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script lang="ts" setup>
import { reactive } from "vue";

// 响应式对象
let car = reactive({ brand: "奔驰", price: 100 });
// 响应式数组
let gameList = reactive([
  { id: "1", name: "英雄联盟" },
  { id: "2", name: "王者荣耀" },
]);

function changePrice() {
  car.price += 10;
}
</script>

3. ref 处理对象类型

ref 也可以接收对象/数组,底层内部会自动调用 reactive,操作依然需要 .value

<script lang="ts" setup>
import { ref } from "vue";

let car = ref({ brand: "宝马", price: 80 });

function changePrice() {
  car.value.price += 5;
}
</script>

4. ref vs reactive 选用原则

  1. 基本类型(字符串、数字、布尔):必须用 ref
  2. 浅层对象/数组ref / reactive 都可以。
  3. 深层嵌套对象:优先 reactive
  4. reactive 禁止直接整体重新赋值,ref 无此限制。

四、toRef 与 toRefs 解构响应式对象

直接解构 reactive 对象会丢失响应式,Vue 提供 toRef / toRefs 解决该问题。

1. toRef:单个属性转换

作用:将响应式对象单个属性转为 ref 对象,保留响应式。

2. toRefs:批量属性转换(常用)

作用:批量将响应式对象所有属性转为 ref,适合解构赋值。

<template>
  <div>
    <h2>姓名:{{ name }}</h2>
    <h2>年龄:{{ age }}</h2>
    <button @click="changeName">修改姓名</button>
  </div>
</template>

<script lang="ts" setup>
import { reactive, toRefs, toRef } from "vue";

let person = reactive({ name: "张三", age: 18, gender: "男" });

// 批量解构(推荐)
const { name, age } = toRefs(person);
// 单个属性解构
const gender = toRef(person, "gender");

function changeName() {
  name.value = "李四";
}
</script>

五、computed 计算属性

根据已有响应式数据衍生出新数据,依赖变化自动重新计算,和 Vue2 用法一致。
分为只读计算属性可读写计算属性两种。

1. 只读计算属性(仅 get)

<template>
  <div>
    <input v-model="firstName" placeholder="姓" />
    <input v-model="lastName" placeholder="名" />
    <p>全名:{{ fullName }}</p>
  </div>
</template>

<script lang="ts" setup>
import { ref, computed } from "vue";

let firstName = ref("张");
let lastName = ref("三");

// 只读计算属性
const fullName = computed(() => {
  return firstName.value + "-" + lastName.value;
});
</script>

2. 可读写计算属性(get + set)

支持读取和手动修改计算属性,修改时会触发 set 逻辑。

<script lang="ts" setup>
import { ref, computed } from "vue";

let firstName = ref("张");
let lastName = ref("三");

const fullName = computed({
  // 读取
  get() {
    return firstName.value + "-" + lastName.value;
  },
  // 修改
  set(val: string) {
    const [f, l] = val.split("-");
    firstName.value = f;
    lastName.value = l;
  },
});

// 主动修改计算属性
fullName.value = "李-四";
</script>

六、watch 侦听器

监听响应式数据变化,执行自定义逻辑。Vue3 中 watch 支持监听 refreactive、函数、数组四类数据。

1. 场景1:监听 ref 基本类型

直接传入变量名即可,可接收新值、旧值。

<script lang="ts" setup>
import { ref, watch } from "vue";

let sum = ref(0);

// 监听 ref 基本类型
watch(
  sum,
  (newVal, oldVal) => {
    console.log("sum变化:", newVal, oldVal);
  }
);

sum.value += 1;
</script>

2. 场景2:监听 ref 对象类型

监听对象内部属性,需要手动开启 deep: true 深度监听。

<script lang="ts" setup>
import { ref, watch } from "vue";

let person = ref({ name: "张三", age: 18 });

// 监听 ref 对象,开启深度监听
watch(
  person,
  (newVal) => {
    console.log("对象变化:", newVal);
  },
  { deep: true }
);

person.value.age = 20;
</script>

3. 场景3:监听 reactive 对象类型

reactive 默认自带深度监听,无需配置 deep

<script lang="ts" setup>
import { reactive, watch } from "vue";

let person = reactive({ name: "张三", age: 18 });

watch(person, (newVal) => {
  console.log("reactive对象变化", newVal);
});

person.age = 22;
</script>

4. 场景4:监听对象单个属性

监听对象中某一个属性,必须写成函数形式

<script lang="ts" setup>
import { reactive, watch } from "vue";

let person = reactive({ name: "张三", age: 18 });

// 监听单个属性(函数写法)
watch(
  () => person.name,
  (newVal) => {
    console.log("姓名变化:", newVal);
  }
);
</script>

5. 场景5:监听多个数据

传入数组,同时监听多个值。

<script lang="ts" setup>
import { ref, reactive, watch } from "vue";

let name = ref("张三");
let person = reactive({ age: 18 });

// 监听多个数据
watch([name, () => person.age], ([newName, newAge]) => {
  console.log("姓名/年龄变化", newName, newAge);
});
</script>

6. 补充配置项

  • immediate: true:组件挂载后立即执行一次监听回调。
  • deep: true:开启深度监听。

七、watchEffect 副作用侦听器

特点

  1. 无需指定监听数据源,自动收集函数内部依赖,依赖变化就执行。
  2. 组件挂载时默认立即执行一次
  3. 返回停止监听函数,可手动关闭监听。

示例

<template>
  <h2>水温:{{ temp }}</h2>
  <h2>水位:{{ height }}</h2>
  <button @click="temp += 10">升温</button>
</template>

<script lang="ts" setup>
import { ref, watchEffect } from "vue";

let temp = ref(0);
let height = ref(0);

// 自动收集依赖 temp、height
const stopWatch = watchEffect(() => {
  if (temp.value >= 50 || height.value >= 20) {
    console.log("触发告警");
  }
  // 满足条件,停止监听
  if (temp.value === 100) stopWatch();
});
</script>

watch 与 watchEffect 对比

  1. watch手动指定监听源,可获取新值/旧值,灵活可控。
  2. watchEffect自动收集依赖,代码简洁,无法获取旧值。

八、标签 ref 与组件传值(defineExpose)

1. 普通 DOM 标签 ref

获取 DOM 元素节点:

<template>
  <h2 ref="title">Vue3 核心语法</h2>
  <button @click="getDom">获取元素</button>
</template>

<script lang="ts" setup>
import { ref } from "vue";

// 变量名和 ref 属性名一致
const title = ref<HTMLHeadingElement | null>(null);

function getDom() {
  console.log(title.value?.innerText);
}
</script>

2. 组件 ref + defineExpose 父子传值

父组件通过 ref 获取子组件实例,子组件必须用 defineExpose 暴露属性/方法,父组件才能访问。

子组件 Child.vue
<script lang="ts" setup>
import { ref, defineExpose } from "vue";

let name = ref("子组件数据");

// 对外暴露数据
defineExpose({ name });
</script>
父组件 App.vue
<template>
  <Child ref="childRef" />
  <button @click="getChildData">获取子组件数据</button>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import Child from "./Child.vue";

const childRef = ref<InstanceType<typeof Child> | null>(null);

function getChildData() {
  console.log(childRef.value?.name);
}
</script>

九、props 父传子(TypeScript 类型约束)

<script setup> 中使用 defineProps 接收父组件传值,支持类型校验、默认值。

1. 基础用法

父组件
<template>
  <Child :list="userList" />
</template>

<script lang="ts" setup>
import { reactive } from "vue";
import Child from "./Child.vue";

interface User {
  id: string;
  name: string;
  age: number;
}

const userList = reactive<User[]>([
  { id: "1", name: "张三", age: 18 },
  { id: "2", name: "李四", age: 19 },
]);
</script>
子组件(三种写法)
<template>
  <div v-for="item in list" :key="item.id">
    {{ item.name }} - {{ item.age }}
  </div>
</template>

<script lang="ts" setup>
import { withDefaults, defineProps } from "vue";

interface User {
  id: string;
  name: string;
  age: number;
}

// 写法1:仅接收,无类型校验
// const props = defineProps(["list"])

// 写法2:TS 类型校验
// const props = defineProps<{ list: User[] }>()

// 写法3:类型校验 + 可选 + 默认值(企业常用)
const props = withDefaults(defineProps<{ list?: User[] }>(), {
  list: () => [{ id: "0", name: "默认用户", age: 0 }],
});
</script>

十、Vue3 生命周期

Vue3 组合式API 生命周期全部以 onXXX 形式导入使用setup 替代了 beforeCreatecreated

生命周期对照表

阶段 Vue2 钩子 Vue3 组合式API 钩子
创建阶段 beforeCreate/created setup(替代)
挂载阶段 beforeMount/mounted onBeforeMount / onMounted
更新阶段 beforeUpdate/updated onBeforeUpdate / onUpdated
卸载阶段 beforeDestroy/destroyed onBeforeUnmount / onUnmounted

完整示例

<template>
  <h2>{{ sum }}</h2>
  <button @click="sum += 1">更新</button>
</template>

<script lang="ts" setup>
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from "vue";

let sum = ref(0);

console.log("setup 执行");

// 挂载前
onBeforeMount(() => console.log("挂载前"));
// 挂载完成(请求接口、操作DOM 常用)
onMounted(() => console.log("挂载完成"));

// 更新前
onBeforeUpdate(() => console.log("数据更新前"));
// 更新完成
onUpdated(() => console.log("数据更新完成"));

// 卸载前(清除定时器、解绑事件 常用)
onBeforeUnmount(() => console.log("组件卸载前"));
// 卸载完成
onUnmounted(() => console.log("组件卸载完成"));
</script>

十一、自定义 Hook

自定义 Hook 是 Vue3 逻辑复用的核心方案,本质就是一个普通 TS 函数,封装 setup 中的组合式 API,替代 Vue2 的 mixin。

1. 编写自定义 Hook

新建 hooks/useCount.ts

// 封装加减逻辑
import { ref, onMounted } from "vue";

export default function useCount() {
  const count = ref(0);

  // 加
  const increment = () => {
    count.value++;
  };
  // 减
  const decrement = () => {
    count.value--;
  };

  // 挂载后初始化
  onMounted(() => {
    count.value = 10;
  });

  // 向外暴露数据和方法
  return { count, increment, decrement };
}

2. 组件中使用 Hook

<template>
  <h2>计数:{{ count }}</h2>
  <button @click="increment">+1</button>
  <button @click="decrement">-1</button>
</template>

<script lang="ts" setup>
// 引入自定义Hook
import useCount from "@/hooks/useCount";

// 解构使用
const { count, increment, decrement } = useCount();
</script>

十二、总结

  1. setup 是组合式API入口,<script setup> 语法糖为官方首选。
  2. 响应式:基本类型用 ref,对象/数组优先 reactive,解构用 toRefs
  3. 计算属性 computed:依赖缓存,适合数据加工;侦听器 watch/watchEffect 适合异步、复杂逻辑。
  4. 生命周期onMounted 处理DOM/接口,onBeforeUnmount 清理副作用。
  5. 自定义Hook:抽离通用逻辑,实现代码复用,是 Vue3 工程化必备。

更多推荐