3、Composition API 入门:setup 函数、ref 与 reactive 响应式基础

好,咱们正式开始啃 Composition API 这块硬骨头。

说实话,我刚从 Options API 切换到 Composition API 时,心里也犯嘀咕——这玩意儿到底好在哪?但用了一段时间后,我只能说:真香。尤其是当你面对一个几百行的组件时,Composition API 那种「按逻辑组织代码」的方式,简直是一种解脱。

3.1 setup 函数:一切从这里开始

在 Vue3 里,setup 是 Composition API 的入口。你可以把它理解成「组件的大脑」——所有逻辑、状态、方法,都在这里初始化。

核心要点:

  • setup 在组件创建之前执行,此时 this 不可用
  • 它接收两个参数:propscontext
  • 返回的对象会暴露给模板使用

来看个最简单的例子:

// 基础 setup 函数
export default {
  setup(props, context) {
    // 这里不能使用 this
    console.log(props.msg)  // 可以访问 props
    console.log(context.attrs) // 非 props 属性

    // 返回给模板
    return {
      message: 'Hello Vue3!'
    }
  }
}

嗯,这里要注意:setup 里千万别用 this。我刚开始迁移时踩过这个坑,在 setup 里写了个 this.$emit,结果控制台报错半天没反应过来。后来才意识到,setup 执行时组件实例还没创建完呢。

3.2 ref:让普通数据变「活」

在 Vue2 里,我们习惯在 data 里定义响应式数据。到了 Vue3,ref 就是干这个活的——它能把一个普通值变成响应式。

我的习惯:对于基本类型(字符串、数字、布尔值),我几乎都用 ref。简单直接,不绕弯子。

import { ref } from 'vue'

export default {
  setup() {
    // 定义一个响应式计数器
    const count = ref(0)

    // 修改值要用 .value
    function increment() {
      count.value++
    }

    return {
      count,
      increment
    }
  }
}

你可能会问:为什么模板里不用写 .value

因为 Vue3 在模板编译时自动解包了 ref。说白了,这是框架帮你做了一层「语法糖」。但在 setup 里,你必须手动加 .value——这个区别我一开始老忘。

避坑指南:我曾经在 setup 里直接写 count = 1,结果页面死活不更新。记住:ref 的值必须通过 .value 修改,否则不会触发响应式更新。

3.3 reactive:处理复杂对象的利器

如果说 ref 是「单兵作战」,那 reactive 就是「集团军」——它专门处理对象、数组这类复杂结构。

import { reactive } from 'vue'

export default {
  setup() {
    // 定义一个响应式对象
    const user = reactive({
      name: '张三',
      age: 25,
      hobbies: ['编程', '读书']
    })

    // 直接修改属性,不需要 .value
    function updateName(newName) {
      user.name = newName
    }

    return {
      user,
      updateName
    }
  }
}

你看,用 reactive 时,修改属性就像操作普通对象一样自然。不需要 .value,也不需要 $set——Vue3 底层用 Proxy 实现了真正的「深层响应」。

关键区别:

特性 ref reactive
适用类型 基本类型、对象 对象、数组
访问方式 需要 .value 直接访问
解包行为 模板自动解包 深层响应
重新赋值 支持 不支持(会丢失响应)

说到重新赋值,我遇到过这么个情况:用 reactive 定义了一个表单对象,后来想整个替换它,直接写了 form = newForm。结果页面没反应。为什么?因为 reactive 返回的是代理对象,你重新赋值相当于把代理丢了。

我的建议:如果你需要整体替换一个响应式对象,要么用 ref 包裹它,要么用 Object.assign 逐个属性更新。

3.4 ref 与 reactive 的选择策略

说实话,这两个 API 怎么选,没有绝对标准。但我个人有个「三原则」:

  1. 基本类型用 ref——简单、直观、不会出错
  2. 复杂对象用 reactive——省去 .value 的麻烦
  3. 需要整体替换时用 ref——避免丢失响应式

你想想看,如果整个组件都用 ref,代码里到处都是 .value,看着多累。反过来,如果所有数据都用 reactive,遇到基本类型又得包一层对象,也不优雅。

// 实际项目中的混合使用
setup() {
  // 基本类型用 ref
  const loading = ref(false)
  const pageSize = ref(20)

  // 复杂对象用 reactive
  const tableData = reactive({
    list: [],
    total: 0,
    currentPage: 1
  })

  // 需要整体替换的用 ref 包裹对象
  const formData = ref({
    name: '',
    age: null
  })

  // 修改时
  loading.value = true
  tableData.list = response.data
  formData.value = { name: '李四', age: 30 }

  return {
    loading,
    pageSize,
    tableData,
    formData
  }
}

嗯,这样混着用,代码反而更清晰。每个变量一看就知道它是什么类型、该怎么操作。

3.5 响应式原理:Proxy 到底做了什么?

你可能好奇:Vue3 的响应式为什么比 Vue2 强?

说白了,Vue2 用的是 Object.defineProperty,它只能拦截属性的「读取」和「赋值」。新增属性、删除属性、数组索引操作——这些它都管不了。所以 Vue2 才需要 $set$delete 这些补丁。

Vue3 换成了 Proxy,它直接代理整个对象。任何操作——读取、赋值、新增、删除、甚至 in 操作符——都能被拦截到。

// 简单模拟 reactive 的原理
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      // 触发更新
      trigger(target, key)
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      // 触发更新
      trigger(target, key)
      return result
    }
  })
}

你看,代码其实不复杂。核心就是「拦截操作 → 收集依赖 → 触发更新」这个流程。我在项目中曾经自己写过一个简化版的响应式系统,虽然只有几十行代码,但跑通的那一刻,对 Vue3 的理解直接上了一个台阶。

注意:虽然 Proxy 很强大,但它不能代理基本类型。这就是为什么 Vue3 还需要 ref——ref 内部其实是用一个对象包裹了基本类型,然后再用 reactive 处理这个包裹对象。

3.6 实战:一个完整的计数器组件

最后,咱们把今天学的串起来,写个完整的例子:

<template>
  <div>
    <h2>计数器:{{ count }}</h2>
    <p>用户信息:{{ user.name }},{{ user.age }}岁</p>
    <button @click="increment">加一</button>
    <button @click="reset">重置</button>
  </div>
</template>

<script>
import { ref, reactive } from 'vue'

export default {
  setup() {
    // ref 处理基本类型
    const count = ref(0)

    // reactive 处理对象
    const user = reactive({
      name: '王五',
      age: 28
    })

    function increment() {
      count.value++
      user.age++
    }

    function reset() {
      count.value = 0
      user.age = 28
    }

    return {
      count,
      user,
      increment,
      reset
    }
  }
}
</script>

这个例子虽然简单,但涵盖了 setuprefreactive 的核心用法。你可以在项目中先从小功能开始尝试,慢慢就会找到感觉。

下一章,咱们聊聊 computedwatch——这两个在 Composition API 里也变了不少。到时候我会分享一些我在实际项目中遇到的「坑」和「妙用」。