Vue3组合式API

Vue 3 组合式 API 完全指南

本文档从零开始讲解 Vue 3 的组合式 API(Composition API):为什么需要它、setuprefreactivecomputedwatch、生命周期、模板 ref、provide/inject,以及组合式函数(Composables)的写法与示例,适合新手系统学习。


一、什么是组合式 API?为什么要有它?

1.1 选项式 API 的局限

在 Vue 2 和 Vue 3 的选项式 API 里,一个组件会拆成 datamethodscomputedmounted 等好几块:

export default {
  data() {
    return { count: 0, name: '' }
  },
  methods: {
    add() {
      this.count++
    }
  },
  mounted() {
    console.log(this.count)
  }
}

当组件变大时,同一块逻辑会散落在不同选项里:和“用户”相关的 data、methods、mounted 混在和“列表”相关的代码中间,按“逻辑”组织代码比较难,复用也主要靠混入(mixins),混入又有命名冲突、来源不清晰等问题。

1.2 组合式 API 在做什么?

组合式 API 让你把与同一功能相关的数据、计算、监听、生命周期写在一起,而不是按 data/methods 拆开。
同时,这些逻辑可以抽成 “组合式函数”(如 useUser、useMouse),在多个组件里复用,且没有混入的合并问题。

组合式 API 的核心包括:

  • refreactive:定义响应式数据。
  • computedwatch:派生与监听。
  • onMountedonUnmounted 等:生命周期钩子。
  • setup:写“组合”逻辑的地方。

下面从 setup 开始讲,再讲 ref/reactivecomputed/watch、生命周期、组合式函数。


二、setup 函数(组合的“入口”)

2.1 基本写法(不用 时)

组件可以导出一个 setup 函数。setup 在组件创建时执行,在 datacomputed 等之前;你在这里定义的数据、方法等,通过 return 暴露给模板和其余选项使用。

export default {
  setup() {
    const count = ref(0)
    function add() {
      count.value++
    }
    return { count, add }
  }
}

模板里就可以用 countadd

setup 还可以接收两个参数:

  • props:当前组件的 props(响应式,不要解构)。
  • context:包含 attrsemitslots 等。
export default {
  setup(props, context) {
    console.log(props.title)
    context.emit('update')
    return { }
  }
}

实际开发中,更常用 ,不用手写 return,写法更简单。


三、(推荐写法)

3.1 是什么?

setup 的语法糖:
里写的顶层变量、函数、import 的组件,会自动暴露给模板使用,不用 return

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="add">+1</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() {
  count.value++
}
</script>
  • countadd 都在顶层声明,模板里直接用。
  • script 里用 count 时要写 count.value(下一节说明)。

3.2 和普通 script 的区别

  • :只能有一个,且里面的代码在 setup 阶段执行,顶层绑定自动暴露给模板。
  • 普通 :可和 同时存在(例如用 export default 写组件名、inheritAttrs 等),但逻辑复用和模板数据一般放在 里。

下文示例若无特别说明,都默认用


四、ref:定义响应式数据

4.1 为什么需要 ref?

在普通 JavaScript 里,改一个变量不会自动更新界面。
Vue 需要“追踪”你在模板和逻辑里用到的数据,所以要用 ref(或 reactive)把数据包成响应式的。

4.2 基本用法

ref(初始值) 会返回一个响应式引用
读取和修改时要写 .value;在 里会自动解包,不用写 .value。

<template>
  <p>{{ count }}</p>
  <button @click="count++">+1</button>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() {
  count.value++
}
</script>
  • 模板里:count 即可,不能写 count.value
  • script 里:count.value 才是当前值,count.value++ 才会触发更新。

4.3 ref 可以包任何类型

  • 数字、字符串、布尔ref(0)ref('')ref(true)
  • 对象、数组ref({ name: 'a' })ref([])
    此时 .value 是对象/数组本身,改 xxx.value.namexxx.value.push(1) 都会触发更新。
const user = ref({ name: '小明', age: 18 })
user.value.name = '小红'
const list = ref([])
list.value.push(1)

4.4 在模板里何时要 .value?

里,顶层的 ref 会自动解包,一般不写 .value。
但在表达式里若把 ref 放进对象或数组里,不会自动解包,例如:

<p>{{ obj.count }}</p>

obj{ count: ref(0) },这里 obj.count 仍然是 ref,需要写成 obj.count.value 或在 script 里用 reactive/toRefs 处理。
日常写法里,模板中直接用顶层 ref 即可。


五、reactive:响应式对象

5.1 基本用法

reactive(对象) 会把整个对象变成响应式的。
在 script 里直接改属性即可,不需要 .value;模板里也是直接写属性名。

<template>
  <p>{{ state.name }} {{ state.age }}</p>
  <button @click="state.age++">年龄+1</button>
</template>

<script setup>
import { reactive } from 'vue'
const state = reactive({
  name: '小明',
  age: 18
})
</script>
  • state 本身不是 ref,所以不能state = xxx 整体替换(会丢响应性)。
  • 只能改属性state.name = ‘小红’state.age++

5.2 reactive 只接受对象类型

reactive 只适合对象(包括数组)。
传数字、字符串等会报警告,应改用 ref

5.3 解构会丢失响应性

reactive 解构出来的变量不再是响应式的:

const state = reactive({ count: 0 })
const { count } = state
count++  // 改的是普通变量,界面不会更新

需要“拆开用”又保持响应式时,用 toRefs

import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'a' })
const { count, name } = toRefs(state)

这样 countnameref,改 count.valuename.value 会同步回 state,模板里用 countname 即可(自动解包)。


六、ref 和 reactive 怎么选?

对比 ref reactive
类型 任意类型 仅对象/数组
读写 script 里要 .value 直接改属性
整体替换 可以 xxx = ref(新值) 不能整体替换,会丢响应性
解构 解构出来仍是 ref 解构会丢响应性,需 toRefs

简单建议:

  • 单值(数字、字符串、布尔、一个数组、一个“根对象”)→ 用 ref
  • 一组强相关的字段(如表单 state)→ 用 reactive 也可以;若希望解构或传给别人用,可 reactive + toRefs,或直接用多个 ref

七、computed:计算属性

7.1 基本用法

computed(() => 返回值) 会根据依赖自动计算缓存;依赖不变就不会重算。
在 script 里用要 .value,在模板里自动解包。

<template>
  <p>双倍:{{ double }}</p>
</template>

<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
</script>

7.2 可写计算属性(get/set)

需要“既能读又能写”时,传对象,写 getset

const fullName = computed({
  get() {
    return firstName.value + ' ' + lastName.value
  },
  set(v) {
    const [first, ...rest] = v.split(' ')
    firstName.value = first
    lastName.value = rest.join(' ')
  }
})

详细用法见《Vue3计算属性.md》。


八、watch 与 watchEffect:监听

8.1 watch(数据源, 回调)

watch 用来在某个数据变化时执行逻辑(如请求接口、存本地)。

import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
  console.log(newVal, oldVal)
})

监听 reactive 的某个属性要用 getter:

const state = reactive({ name: '' })
watch(() => state.name, (newVal) => {
  console.log(newVal)
})

可加 { deep: true } 深度监听对象,{ immediate: true } 立即执行一次。详见《Vue3监听属性.md》。

8.2 watchEffect(回调)

watchEffect立刻执行一次回调,并收集回调里用到的响应式数据作为依赖;之后任一代变化就再执行。

import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
  console.log(count.value)
})

不需要“旧值”、且希望“一进来就跑一次”时,用 watchEffect 较方便。


九、生命周期钩子

在组合式 API 里,生命周期用 onXxx 的形式按需注册

选项式 API 组合式 API
beforeCreate / created 无,逻辑直接写在 setup 里
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted

示例:

<script setup>
import { onMounted, onUnmounted } from 'vue'

onMounted(() => {
  console.log('挂载完成')
})

onUnmounted(() => {
  console.log('即将卸载,可清理定时器、监听')
})
</script>

可写多个 onMounted,都会执行;一般把“和某块逻辑相关”的清理放在对应的 onUnmounted 里。


十、模板 ref:拿到 DOM 或子组件实例

10.1 在模板上写 ref=”xxx”

里给元素或组件加 ref=”变量名”,在 里用 ref(null) 声明同名变量,挂载后 变量.value 就是该 DOM 元素组件实例

<template>
  <input ref="inputRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
  inputRef.value?.focus()
})
</script>
  • inputRef.value 在挂载后才是 DOM,所以一般在 onMounted 里用。
  • 若用 v-if 控制显示,可能要在 nextTick 后再访问。

10.2 在 v-for 里用 ref

refv-for 里会得到数组(每个元素对应一项);若只关心一个列表的“容器”DOM,可给外层包一个元素并 ref 到该元素上。


十一、provide / inject:跨层级传数据

11.1 作用

provide祖先提供数据,inject任意后代注入,无需层层 props。

11.2 基本用法

祖先:

import { ref, provide } from 'vue'
const theme = ref('dark')
provide('theme', theme)

后代:

import { inject } from 'vue'
const theme = inject('theme')

inject 可以写默认值:inject(‘theme’, ‘light’)
适合主题、语言、用户信息等“全局性”数据。详见《Vue3组件.md》。


十二、组合式函数(Composables):逻辑复用

12.1 什么是组合式函数?

组合式函数就是用组合式 API 写出来的、可复用的函数
命名一般以 use 开头,如 useMouseuseFetch
函数里用 refreactiveonMountedwatch 等,把“数据 + 方法”return 出去,组件里调用就能得到同样的能力。

12.2 示例:useCounter

// composables/useCounter.js
import { ref } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  function add() {
    count.value++
  }
  function minus() {
    count.value--
  }
  return { count, add, minus }
}

组件里:

<template>
  <p>{{ count }}</p>
  <button @click="add">+1</button>
  <button @click="minus">-1</button>
</template>

<script setup>
import { useCounter } from './composables/useCounter'
const { count, add, minus } = useCounter(10)
</script>

12.3 示例:useMouse(监听鼠标位置)

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

组件里:

<template>
  <p>鼠标:{{ x }}, {{ y }}</p>
</template>

<script setup>
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>

12.4 示例:useFetch(请求列表)

// composables/useFetch.js
import { ref, onMounted } from 'vue'
import axios from 'axios'

export function useFetch(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref('')
  async function run() {
    loading.value = true
    error.value = ''
    try {
      const res = await axios.get(url)
      data.value = res.data
    } catch (e) {
      error.value = e.message || '请求失败'
    } finally {
      loading.value = false
    }
  }
  onMounted(run)
  return { data, loading, error, run }
}

组件里:

<template>
  <p v-if="loading">加载中...</p>
  <p v-else-if="error">{{ error }}</p>
  <ul v-else>
    <li v-for="item in data" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

<script setup>
import { useFetch } from './composables/useFetch'
const { data, loading, error, run } = useFetch('/api/list')
</script>

十三、与选项式 API 混用(了解即可)

同一组件可以同时有 和 选项式 datamethods 等。
组合式 API 的 ref 等会合并,选项式里可通过 this 访问;但实际开发中更推荐统一用组合式 API,避免混用增加心智负担。


十四、常见问题与注意点

14.1 ref 在 script 里必须 .value

里,ref 是引用对象,读写都要 .value;模板里不用。

14.2 reactive 不能整体替换

state = reactive({ … }) 会断掉响应式;只能改属性
若确实要“换一整份数据”,可考虑用 ref 包对象:state.value = { … }

14.3 解构 reactive 要用 toRefs

const { count } = reactive({ count: 0 })count 不是响应式的;要用 toRefs 转成 ref 再解构。

14.4 生命周期在 setup 里按顺序注册

多个 onMounted 会按注册顺序执行;逻辑按“功能块”组织即可。


十五、组合式 API 速查表

需求 API
响应式数据(任意类型) ref(初始值),script 里 .value
响应式对象 reactive(对象),直接改属性
解构 reactive 仍响应式 toRefs(reactive对象)
计算属性 computed(() => 值)
监听 watch(源, 回调)、watchEffect(回调)
挂载后 onMounted(fn)
卸载前清理 onUnmounted(fn)
模板 ref ref(null),模板 ref=”同名”
跨层级传值 provide(key, value)、inject(key)
逻辑复用 写 useXxx() 组合式函数,return { … }

十六、学习建议

  1. 先熟练 ref(含 .value)和 的写法,能写一个带数据与方法的页面。
  2. 再练 reactivetoRefs,理解“解构会丢响应性”。
  3. computedwatchonMounted/ onUnmounted 按需用;有需要再查《Vue3计算属性》《Vue3监听属性》。
  4. 逻辑复用优先写 useXxx 组合式函数,替代混入;多写几个 useCounter、useMouse、useFetch 类的例子,体会“按功能组织 + 复用”。

把本文档里的示例在项目里敲一遍、改一改,会掌握得更牢。祝你学习顺利。

发表评论