二、Vue3 核心基础:API 对比、Setup 与响应式详解

文章目录

    • 一、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. 什么是 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

  • 结果: 你的视线必须在 datamethodscomputed 之间来回上下"反复横跳",一个业务的代码被拆得七零八落。当组件达到几百行时,维护起来非常痛苦。

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( )函数有两个致命的痛点

  1. 必须手动 returnsetup 里面定义了变量和方法,最后必须写一句 return { name, age, ... }。如果不写,<template> 模板就拿不到,页面就会报错。

  2. 此时还不是响应式: 就像你代码里注释写的,这时候用 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 中的 refreactive 是组合式 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 变成了普通对象。

正确解法:

  1. 改用 ref(推荐):const userInfo = ref({ name: 'Tom' }); userInfo.value = { name: 'Jerry' };

  2. 或者不改引用,只改属性: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 会导致响应式丢失,那如果我们非要解构、或者想把对象的某个属性单独传给子组件,该怎么办?这就需要 toReftoRefs

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 上。
相关推荐
问心无愧051318 小时前
ctf show web入门160 161
前端·笔记
李小白6618 小时前
第四天-WEB服务器基本原理,IIS服务
运维·服务器·前端
humcomm18 小时前
AI编程时代新前端职位
前端·ai编程
好家伙VCC19 小时前
Web Components主题热切换方案揭秘
java·前端
甲维斯19 小时前
Kimi版超级玛丽效果“惊人”,配额不足5厘米!
前端·人工智能
hboot19 小时前
AI工程师第一课 - Python
前端·后端·python
凉菜凉凉19 小时前
AI时代,被抛弃的前端
前端·ai
console.log('npc')20 小时前
AI前端工程与生成式UI学习路线
前端·人工智能·ui
huangdong_20 小时前
淘宝商品SKU图自动分类技术深度解析:从DOM解析到智能归档
开发语言·javascript·ecmascript
梦曦i20 小时前
uni-router v1.1.1发布:守卫超时保护+路由监听
前端·uni-app