文章目录
-
- 一、OptionsAPI与CompositionAPI
-
- [1. 核心概念对比](#1. 核心概念对比)
-
- [Options API (选项式 API)](#Options API (选项式 API))
- [Composition API (组合式 API)](#Composition API (组合式 API))
- [2. 核心区别:代码组织方式](#2. 核心区别:代码组织方式)
-
- [Options API 的痛点:碎片化](#Options API 的痛点:碎片化)
- [Composition API 的优势:高内聚](#Composition API 的优势:高内聚)
- [3. 代码直观对比](#3. 代码直观对比)
-
- [方式 A:Options API 风格](#方式 A:Options API 风格)
- [方式 B:Composition API 风格 (Vue 3 推荐的 `<script setup>`)](#方式 B:Composition API 风格 (Vue 3 推荐的
<script setup>))
- 二、setup
-
- [1. 原始的 `setup()` 函数(为什么麻烦?)](#1. 原始的
setup()函数(为什么麻烦?)) - [2. `setup` 语法糖(消除 return)](#2.
setup语法糖(消除 return)) - 扩展:"给组件起名字"
- [1. 原始的 `setup()` 函数(为什么麻烦?)](#1. 原始的
- 三、响应式数据
-
- [1. 什么是 ref?](#1. 什么是 ref?)
- [2. 什么是 reactive?](#2. 什么是 reactive?)
- [3. ref vs reactive 大比拼(核心区别)](#3. ref vs reactive 大比拼(核心区别))
- [4. 为什么 reactive 容易让人踩雷?](#4. 为什么 reactive 容易让人踩雷?)
- [5. 最佳实践:我该选哪一个?](#5. 最佳实践:我该选哪一个?)
- 四、toRefs,toRef
一、OptionsAPI与CompositionAPI
在 Vue 的发展历程中,Options API(选项式 API)和 Composition API(组合式 API)是两种最核心的组件编写风格。Vue 2 时代主要是 Options API 的天下,而 Vue 3 引入了 Composition API 并将其推为主流。
用一句话总结它们的区别:Options API 是"按代码特性分堆",而 Composition API 是"按业务逻辑分堆"。
1. 核心概念对比
Options API (选项式 API)
Options API 就像是给你一套固定的模版。你需要把代码填进特定的"选项"(Options)里:
-
data放响应式数据 -
methods放函数方法 -
computed放计算属性 -
watch放侦听器
Composition API (组合式 API)
Composition API 则给你了绝对的自由 。它不再限制你代码的位置,而是提供了一系列核心方法(如 ref, reactive, computed, onMounted 等),让你直接在 setup 函数(或 <script setup>)中自由组合它们。
2. 核心区别:代码组织方式
为了更直观地理解,我们可以通过"高亮代码块"的分布来看看它们在处理复杂业务时的差异:
Options API 的痛点:碎片化
假设一个组件同时包含"用户管理"和"商品列表"两个业务逻辑:
-
在 Options API 中,用户管理的变量在
data,方法在methods;商品列表的变量也在data,方法也在methods。 -
结果: 你的视线必须在
data、methods、computed之间来回上下"反复横跳",一个业务的代码被拆得七零八落。当组件达到几百行时,维护起来非常痛苦。
Composition API 的优势:高内聚
-
在 Composition API 中,你可以把"用户管理"的所有数据、方法、生命周期写在一起;把"商品列表"的所有代码写在一起。
-
你甚至可以把整个业务逻辑抽离成一个独立的
useUser.js文件(即自定义 Hook),在任何组件里直接引入。
3. 代码直观对比
我们来实现同一个功能:一个简单的计数器,加上一个改变标题的功能。
方式 A:Options API 风格
html
<script>
export default {
data() {
return {
count: 0,
title: 'Hello Vue'
};
},
computed: {
doubleCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
},
changeTitle() {
this.title = 'Title Changed!';
}
}
};
</script>
方式 B:Composition API 风格 (Vue 3 推荐的 <script setup>)
html
<script setup>
import { ref, computed } from 'vue';
// 逻辑一:计数器业务(高内聚在一起)
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
// 逻辑二:标题业务(高内聚在一起)
const title = ref('Hello Vue');
function changeTitle() {
title.value = 'Title Changed!';
}
</script>
二、setup
1. 原始的 setup() 函数(为什么麻烦?)
html
<script lang="ts">
import { defineComponent, ref } from 'vue'
// 使用 defineComponent 定义组件
export default defineComponent({
// setup 函数是组合式API的入口
setup() {
// 1. 定义响应式变量
let name = ref('张三')
let age = ref(18)
let tel = '13888888888' // 非响应式
// 2. 定义方法
function changeName() {
name.value = 'zhang-san'
}
function changeAge() {
age.value += 1
}
function showTel() {
alert(tel)
}
// 3. 必须 return 出去,模板才能使用
return {
name,
age,
tel,
changeName,
changeAge,
showTel
}
}
})
</script>
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
</div></template>
在 Vue 3 刚出来的时候,setup( )函数有两个致命的痛点:
-
必须手动
return: 在setup里面定义了变量和方法,最后必须写一句return { name, age, ... }。如果不写,<template>模板就拿不到,页面就会报错。 -
此时还不是响应式: 就像你代码里注释写的,这时候用
let name = '张三',点击按钮虽然改变了变量,但页面死活不转。
因为每次都要写 return 太痛苦了,所以官方后来推出了语法糖。
2. setup 语法糖(消除 return)
写法:直接在 <script> 标签上加一个 setup 单词。
有了它,刚才那一大堆复杂的代码,直接缩减成了这样:
html
<script setup lang="ts">
// 1. 变量和方法直接定义,不需要写 export default {}
// 2. 也不需要写 setup() 函数的外壳
// 3. 配合 ref 让它变成真正的响应式
import { ref } from 'vue'
let name = ref('张三')
let age = ref(18)
let tel = '13888888888' // 电话不需要改,写成非响应式
function changeName() {
name.value = 'zhang-san' // 真正的响应式修改
}
function changeAge() {
age.value += 1
}
function showTel() {
alert(tel)
}
// 注意:这里绝对不需要写 return !!!模板自动就能用。
</script>
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
</div>
</template>
扩展:"给组件起名字"
现在已经不需要安装第三方插件了 !Vue 3.3+ 官方已经原生支持了一个叫
defineOptions的宏命令。标准做法是这样:
html
<script setup lang="ts">
// 官方自带的起名神器,不需要装任何插件
defineOptions({ name: 'Person' })
</script>
-
绝大多数时候:可以完全忘记组件命名这件事,放心地交给文件名自动推导。
-
只有在需要递归、需要
KeepAlive精准控制、或者用 Vue Devtools 调试在成百上千个组件里找瞎眼时 :defineOptions({ name: 'xxx' })才用得到。
三、响应式数据
Vue3 中的 ref 和 reactive 是组合式 API(Composition API)中最核心的两个响应式数据核心工具。很多刚从 Vue2 转过来的同学容易搞混它们。
其实很简单,我们可以把它们理解为"单值包装盒"和"对象代理器"。下面为你详细拆解。
1. 什么是 ref?
ref 主要用来定义基本数据类型 (如 String, Number, Boolean, null 等)的响应式数据,但它其实也可以传对象。
-
本质 :它把你的值包裹在一个对象的
value属性里。 -
特点:
-
在 JS/TS 中 修改或读取它,必须加上
.value。 -
在 Template(模板)中 读取它,Vue 会自动解包,不需要 写
.value。
-
html
<script setup>
import { ref } from 'vue'
// 定义响应式数据
const count = ref(0)
const name = ref('Vue3')
function increment() {
count.value++ // JS中必须用 .value
}
</script>
<template>
<button @click="increment">点击了 {{ count }} 次</button>
<p>你好,{{ name }}</p>
</template>
2. 什么是 reactive?
reactive 只能用来定义复杂数据类型 (如 Object, Array, Map, Set),不能传基本类型。
-
本质 :它是基于底层
Proxy实现的,直接对目标对象进行深度响应式代理。 -
特点:
-
无论在 JS 还是 Template 中,都不需要
.value,直接通过属性访问。 -
致命痛点 :不能直接替换整个对象,也不能直接解构,否则会失去响应式。
-
html
<script setup>
import { reactive } from 'vue'
// 定义对象
const state = reactive({
count: 0,
user: { name: '张三' } // 深度响应式
})
function increment() {
state.count++ // 直接修改,不需要 .value
}
// 错误示范 1:直接赋值一个新对象(会导致响应式丢失)
function reset() {
// state = { count: 0, user: { name: '张三' } } // 这会打破 Proxy 的代理!
}
// 错误示范 2:解构赋值
// const { count } = state // 此时 count 变成了一个普通数字,不再是响应式的
</script>
<template>
<button @click="increment">点击了 {{ state.count }} 次</button>
</template>
3. ref vs reactive 大比拼(核心区别)
为了让你一目了然,我们用一张表格来对比它们的应用场景和特性:
| 特性 / 维度 | ref | reactive |
|---|---|---|
| 推荐数据类型 | 基本类型 (Number, String 等) 以及数组 |
复杂的对象 (Object) |
| 底层实现 | RefImpl 类的 get/set 拦截(若传对象,内部也会调 reactive) |
基于 ES6 的 Proxy 进行拦截 |
| JS中访问 | 必须加 .value |
不需要 .value,直接 obj.key |
| Template中访问 | 自动解包,不需要 .value |
直接 obj.key |
| 重新赋值(覆盖) | 支持 。例:state.value = {} 依然是响应式的 |
不支持直接覆盖整个对象,会丢失响应式 |
| 解构赋值 | 不支持(但可以通过 toRefs 转化后解构) |
不支持(解构后会变成普通变量,失去响应式) |
4. 为什么 reactive 容易让人踩雷?
很多新手喜欢用 reactive,因为觉得"不用写 .value 太爽了"。但 reactive 有两个非常容易让人抓狂的限制:
直接赋值导致响应式"断开"
如果你从后端请求了一个新对象,想赋值给 reactive:
js
let userInfo = reactive({ name: 'Tom', age: 18 })
// 后来从后端拿到了新数据
userInfo = { name: 'Jerry', age: 20 } // 这样写,页面绝对不会更新!userInfo 变成了普通对象。
正确解法:
-
改用
ref(推荐):const userInfo = ref({ name: 'Tom' }); userInfo.value = { name: 'Jerry' }; -
或者不改引用,只改属性:
Object.assign(userInfo, newSubObj)
解构赋值丢失响应式
js
const pos = reactive({ x: 0, y: 0 })
const { x, y } = pos // 此时 x 和 y 只是普通的数字 0,你修改它们,页面不会变。
正确解法:
使用 toRefs 帮它套一层 ref:
js
import { reactive, toRefs } from 'vue'
const pos = reactive({ x: 0, y: 0 })
const { x, y } = toRefs(pos) // 现在 x 和 y 是 ref 了,在 JS 中使用需要 x.value
5. 最佳实践:我该选哪一个?
目前前端社区更推荐"一把梭"优先使用 ref。
-
官方推荐/社区主流 :优先使用
ref。无论是数字、字符串还是对象、数组,通通可以用ref。虽然写.value稍微有点繁琐,但它绝对不会因为赋值或解构导致响应式丢失,心智负担极低。 -
何时用
reactive:只有当一组数据关系非常紧密,比如表单的数据绑定(const loginForm = reactive({ username: '', password: '' })),且你确定不会去整体替换这个表单对象时,用reactive会让代码看起来更干净。
四、toRefs,toRef
既然解构 reactive 会导致响应式丢失,那如果我们非要解构、或者想把对象的某个属性单独传给子组件,该怎么办?这就需要 toRef 和 toRefs。
html
<script setup>
import { reactive, toRef, toRefs } from 'vue'
const form = reactive({
title: '服务器故障',
priority: 'high',
count: 1
})
// 1. toRef:把对象里的【单个属性】提取出来,变成一个 ref 对象
const priorityRef = toRef(form, 'priority')
// 2. toRefs:把对象里的【所有属性】批量转成 ref,然后可以安全地解构(最常用)
const { title, count } = toRefs(form)
const modify = () => {
// 此时它们全都是真正的 ref 了,修改它们,原 form 对象里的数据也会同步改变!
title.value = 'MySQL 死锁故障'
priorityRef.value = 'critical'
}
</script>
<template>
<p>工单:{{ title }} ------ 紧急度:{{ priorityRef }}</p>
<button @click="modify">修改</button>
</template>
- 底层逻辑 :
toRef(s)并没有创建任何新的响应式对象,它只是做了一层桥接(指向代理) 。它创建了一个临时的 ref 对象,这个 ref 内部的.value读写操作,本质上是被拦截重定向到了原reactive对象的对应 key 上。