Vue3常用的组合式API 超详细讲解

一、响应式基础 API:ref /reactive(底层 + 用法 + 避坑)

这是组合式 API 的基石,先搞懂底层原理,才能真正用好!

1. 核心原理铺垫

Vue3 的响应式系统基于 ES6 Proxy (替代 Vue2 的Object.defineProperty),核心是「拦截数据的读取 / 修改操作,触发依赖收集和视图更新」:

  • reactive:直接对对象 / 数组创建 Proxy 代理,拦截属性的读写;
  • ref:对基本类型 (String/Number/Boolean)包装成一个「含.value属性的对象」,再通过 Proxy 代理这个对象的.value

这就是为什么ref需要.value------ 本质是代理了包装对象的.value属性,而不是原始值本身。

2. ref:基本类型 + 兼容复杂类型

核心作用
  • 优先处理基本类型(String/Number/Boolean/undefined/null);
  • 兼容处理复杂类型 (对象 / 数组):内部会自动转为reactive代理。
底层结构(简化版)
javascript 复制代码
// ref的简化实现逻辑
function ref(initialValue) {
  // 创建包装对象,含.value属性
  const wrapper = {
    value: initialValue
  }
  // 对包装对象创建Proxy,拦截.value的读写
  return new Proxy(wrapper, {
    get(target, key) {
      if (key === 'value') {
        track(target, 'get', key) // 依赖收集
        return target[key]
      }
    },
    set(target, key, newValue) {
      if (key === 'value') {
        target[key] = newValue
        trigger(target, 'set', key) // 触发更新
        return true
      }
    }
  })
}
分层用法示例
(1)基础用法:基本类型
vue 复制代码
<script setup>
import { ref } from 'vue'

// 1. 定义基本类型响应式数据
const username = ref('张三') // String
const age = ref(18) // Number
const isStudent = ref(true) // Boolean
const emptyVal = ref(null) // null

// 2. 访问/修改:逻辑中必须加 .value
const updateUser = () => {
  username.value = '李四' // 触发响应式更新
  age.value += 2 // 18 → 20
  isStudent.value = false // 切换状态
}

// 3. 模板中:无需 .value(Vue自动解析包装对象)
</script>

<template>
  <p>姓名:{{ username }}</p>
  <p>年龄:{{ age }}</p>
  <button @click="updateUser">修改信息</button>
</template>
(2)进阶用法:复杂类型(对象 / 数组)
vue 复制代码
<script setup>
import { ref } from 'vue'

// 定义复杂类型(内部自动转为reactive)
const user = ref({
  name: '张三',
  address: {
    city: '北京',
    area: '朝阳区'
  }
})
const hobbyList = ref(['篮球', '游戏'])

// 修改复杂类型:先通过 .value 拿到reactive对象,再操作属性
const updateUserInfo = () => {
  user.value.name = '李四' // 直接修改属性(响应式)
  user.value.address.city = '上海' // 深层属性也支持
  hobbyList.value.push('旅行') // 数组方法(push/pop/splice等)
}

// 替换整个对象(响应式不丢失)
const replaceUser = () => {
  user.value = {
    name: '王五',
    address: { city: '广州' }
  }
}
</script>
避坑要点
  • ❌ 不要直接覆盖 ref 对象:age = 20(会丢失 Proxy 代理,响应式失效),必须用age.value = 20
  • ✅ 复杂类型的.value是 reactive 对象,修改深层属性无需再次.value
  • ✅ TypeScript 中自动推导类型,无需额外定义(如const age = ref(18)自动推导为Ref<number>)。

3. reactive:仅复杂类型(对象 / 数组)

核心作用

专门处理对象 / 数组 ,直接创建 Proxy 代理,无需.value,更符合直觉。

底层结构(简化版)
javascript 复制代码
// reactive的简化实现逻辑
function reactive(target) {
  // 仅对对象/数组生效,基本类型直接返回
  if (typeof target !== 'object' || target === null) {
    return target
  }
  // 创建Proxy代理,拦截属性读写
  return new Proxy(target, {
    get(target, key) {
      track(target, 'get', key) // 依赖收集
      // 深层对象递归代理(如user.address.city)
      const result = Reflect.get(target, key)
      return typeof result === 'object' ? reactive(result) : result
    },
    set(target, key, newValue) {
      Reflect.set(target, key, newValue) // 设置属性
      trigger(target, 'set', key) // 触发更新
      return true
    }
  })
}
分层用法示例
(1)基础用法:对象
vue 复制代码
<script setup>
import { reactive } from 'vue'

// 定义对象类型响应式数据
const user = reactive({
  name: '张三',
  age: 18,
  isStudent: true
})

// 修改数据:直接操作属性(无需.value)
const updateUser = () => {
  user.name = '李四'
  user.age = 20
}
</script>
(2)进阶用法:数组 + 深层对象
vue 复制代码
<script setup>
import { reactive } from 'vue'

// 数组类型
const todoList = reactive([
  { id: 1, text: '学习Vue3', done: false },
  { id: 2, text: '掌握组合式API', done: false }
])

// 深层对象
const company = reactive({
  name: 'Vue科技',
  address: {
    province: '北京',
    city: '海淀区',
    detail: '中关村'
  },
  departments: ['前端', '后端', '产品']
})

// 数组操作(响应式)
const addTodo = () => {
  todoList.push({ id: 3, text: '实战项目', done: false })
}
const toggleTodo = (id) => {
  const todo = todoList.find(item => item.id === id)
  todo.done = !todo.done
}

// 深层对象修改(响应式)
const updateAddress = () => {
  company.address.city = '上海'
  company.departments.push('设计')
}
</script>
避坑要点
  • ❌ 不要用于基本类型:const age = reactive(18)(返回原始值 18,无响应式);
  • ❌ 不要替换整个对象:user = { name: '李四' }(会覆盖 Proxy 代理,响应式失效),只能修改属性;
  • ✅ 深层对象自动递归代理:无需手动处理嵌套属性;
  • ✅ 新增属性支持响应式:user.gender = '男'(Vue2 中需用Vue.set,Vue3 无需)。

4. ref vs reactive 终极选择指南

场景 首选 API 原因
基本类型(String/Number/Boolean) ref reactive 不支持基本类型
简单对象(属性少、无需解构) reactive 无需.value,代码简洁
复杂对象(需解构、需替换整个对象) ref 解构不丢响应式,支持直接替换
TypeScript 开发 ref 类型推导更友好(如Ref<number>
数组类型 两者均可 ref 需.value,reactive 更直观

二、响应式派生 API:computed(缓存 + 联动 + 双向绑定)

核心作用

基于响应式数据生成「派生值」,并缓存结果(依赖不变时不重复计算),支持「只读」和「可写」两种模式。

底层原理

computed内部维护了一个「依赖追踪 + 缓存机制」:

  1. 首次访问computed值时,执行计算函数,收集依赖(如count);
  2. 依赖数据变化时,标记computed为 "脏值",下次访问时重新计算;
  3. 依赖不变时,直接返回缓存的结果,提升性能。

分层用法示例

(1)基础用法:只读计算属性(最常用)
vue 复制代码
<script setup>
import { ref, computed } from 'vue'

// 原始响应式数据
const count = ref(0)
const price = ref(100)
const discount = ref(0.8)

// 只读计算属性:依赖count/price/discount
const totalPrice = computed(() => {
  console.log('计算总价(仅依赖变化时执行)')
  return (count.value * price.value * discount.value).toFixed(2)
})

// 多次访问totalPrice,仅首次/依赖变化时执行计算函数
const logTotal = () => {
  console.log(totalPrice.value) // 依赖不变时,直接返回缓存值
  console.log(totalPrice.value) // 不执行计算函数
}
</script>

<template>
  <p>数量:{{ count }}</p>
  <p>单价:{{ price }}</p>
  <p>折扣:{{ discount }}</p>
  <p>总价:{{ totalPrice }}</p> <!-- 模板中直接用,无需.value -->
  <button @click="count++">增加数量</button>
  <button @click="logTotal">打印总价</button>
</template>
(2)进阶用法:可写计算属性(双向绑定)

支持通过修改计算属性,反向更新原始响应式数据,适用于「表单联动」等场景。

vue 复制代码
<script setup>
import { ref, computed } from 'vue'

// 原始数据:姓+名
const firstName = ref('张')
const lastName = ref('三')

// 可写计算属性:全名(get获取,set修改)
const fullName = computed({
  // get:根据firstName/lastName生成全名
  get() {
    return `${firstName.value}${lastName.value}`
  },
  // set:修改全名时,反向拆分给firstName/lastName
  set(newValue) {
    // 假设输入格式为「姓名」(2个字)
    if (newValue.length === 2) {
      firstName.value = newValue[0]
      lastName.value = newValue[1]
    }
  }
})

// 修改计算属性,触发set方法
const updateFullName = () => {
  fullName.value = '李四' // 会触发set,firstName='李',lastName='四'
}
</script>

<template>
  <p>全名:{{ fullName }}</p>
  <p>姓:{{ firstName }}</p>
  <p>名:{{ lastName }}</p>
  <input v-model="fullName" placeholder="输入全名"> <!-- 双向绑定 -->
  <button @click="updateFullName">修改为李四</button>
</template>
(3)实战场景:过滤 + 排序列表
vue 复制代码
<script setup>
import { ref, computed } from 'vue'

// 原始数据:列表+筛选条件
const list = ref([
  { name: 'Vue3', type: '前端', score: 95 },
  { name: 'React', type: '前端', score: 90 },
  { name: 'Node.js', type: '后端', score: 88 },
  { name: 'Python', type: '后端', score: 92 }
])
const filterType = ref('all') // 筛选类型:all/前端/后端
const sortBy = ref('score') // 排序字段:score/name

// 计算属性:过滤+排序后的列表
const filteredList = computed(() => {
  // 1. 过滤
  let result = list.value.filter(item => {
    return filterType.value === 'all' ? true : item.type === filterType.value
  })
  // 2. 排序
  result.sort((a, b) => {
    if (sortBy.value === 'score') {
      return b.score - a.score // 分数降序
    } else {
      return a.name.localeCompare(b.name) // 名称升序
    }
  })
  return result
})
</script>

<template>
  <div>
    <select v-model="filterType">
      <option value="all">全部</option>
      <option value="前端">前端</option>
      <option value="后端">后端</option>
    </select>
    <select v-model="sortBy">
      <option value="score">按分数排序</option>
      <option value="name">按名称排序</option>
    </select>
    <ul>
      <li v-for="item in filteredList" :key="item.name">
        {{ item.name }}({{ item.type }})- 分数:{{ item.score }}
      </li>
    </ul>
  </div>
</template>

避坑要点

  • ❌ 不要在 computed 中执行副作用操作(如修改数据、请求接口、打印日志),仅用于计算派生值;
  • ✅ 依赖数据必须是响应式的(ref/reactive),否则 computed 不会更新;
  • ✅ 可写 computed 必须同时配置 get 和 set,否则修改时会报错;
  • ✅ 复杂计算逻辑建议抽离为单独函数,computed 中仅调用(保持简洁)。

三、响应式监听 API:watch /watchEffect/watchPostEffect /watchSyncEffect

监听响应式数据变化并执行逻辑,核心区别在于「监听源指定方式」和「执行时机」。

1. 核心对比表(先记结论)

API 监听源指定 执行时机 旧值获取 依赖收集 适用场景
watch 手动指定(如 count、()=>user.name) 同步执行(默认) 支持(newVal, oldVal) 仅监听指定源 需要旧值、精准监听单个 / 多个源
watchEffect 自动收集(回调中用到的响应式数据) 同步执行(默认) 不支持 自动收集依赖 无需旧值、监听多个分散源
watchPostEffect 自动收集 DOM 更新后执行 不支持 自动收集依赖 需要基于更新后的 DOM 执行逻辑
watchSyncEffect 自动收集 同步执行(强制) 不支持 自动收集依赖 需要同步执行(如修改 DOM 前的准备)

2. watch:精准监听(支持旧值 + 多源)

底层原理

手动指定监听源,Vue 会追踪源数据的变化,当源变化时执行回调,支持「深度监听」「立即执行」等配置。

分层用法示例
(1)基础用法:监听单个 ref 数据
vue 复制代码
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

// 监听单个ref数据
watch(count, (newVal, oldVal) => {
  console.log(`count从${oldVal}变成${newVal}`)
}, {
  immediate: true, // 组件挂载时立即执行一次(默认false)
  deep: false // 基本类型无需深度监听
})
</script>
(2)进阶用法 1:监听 reactive 对象(深度监听)
vue 复制代码
<script setup>
import { reactive, watch } from 'vue'

const user = reactive({
  name: '张三',
  address: {
    city: '北京',
    area: '朝阳区'
  }
})

// 监听整个reactive对象(自动深度监听,可省略deep: true)
watch(user, (newUser, oldUser) => {
  // 注意:newUser和oldUser是同一个对象(因为Proxy代理的是原对象)
  console.log('user变化:', newUser.address.city)
}, {
  immediate: false,
  deep: true // 监听对象必须开启深度监听(默认true)
})

// 精准监听对象的单个属性(性能更优,无需深度监听)
watch(
  () => user.address.city, // 监听函数(返回要监听的属性)
  (newCity, oldCity) => {
    console.log(`城市从${oldCity}变成${newCity}`) // 支持旧值
  }
)
</script>
(3)进阶用法 2:监听多个数据源
vue 复制代码
<script setup>
import { ref, reactive, watch } from 'vue'

const count = ref(0)
const user = reactive({ name: '张三' })

// 监听多个源:数组形式
watch(
  [count, () => user.name], // 第一个源:count,第二个源:user.name
  ([newCount, newName], [oldCount, oldName]) => {
    console.log(`count: ${oldCount}→${newCount},name: ${oldName}→${newName}`)
  },
  { immediate: true }
)
</script>
避坑要点
  • ❌ 监听 reactive 对象时,newVal 和 oldVal 是同一个对象(因为 Proxy 代理的是原对象),无法通过 oldVal 获取变化前的状态(需手动缓存);
  • ✅ 监听对象的单个属性时,用「监听函数」(() => user.address.city),性能更优;
  • ✅ 基本类型无需深度监听,对象 / 数组必须开启deep: true(监听整个对象时默认开启);
  • ✅ 立即执行(immediate: true)时,首次执行的 oldVal 为undefined

3. watchEffect:自动收集依赖(简洁高效)

底层原理

无需指定监听源,Vue 会自动收集回调函数中「用到的所有响应式数据」作为依赖,当任意依赖变化时,重新执行回调。

分层用法示例
(1)基础用法:自动收集依赖
vue 复制代码
<script setup>
import { ref, reactive, watchEffect } from 'vue'

const count = ref(0)
const user = reactive({ name: '张三' })

// 自动收集依赖:count和user.name
watchEffect(() => {
  console.log(`count: ${count.value},name: ${user.name}`)
})

// 修改任意依赖,都会触发回调
const updateData = () => {
  count.value++ // 触发回调
  // user.name = '李四' // 也会触发回调
}
</script>
(2)进阶用法 1:停止监听 + 清理副作用
vue 复制代码
<script setup>
import { ref, watchEffect } from 'vue'

const inputVal = ref('')

// watchEffect返回停止函数
const stopWatch = watchEffect((onInvalidate) => {
  // 模拟接口请求(副作用)
  const timer = setTimeout(() => {
    console.log('搜索:', inputVal.value)
  }, 500)

  // 清理函数:依赖变化/组件卸载时执行(避免重复请求)
  onInvalidate(() => {
    clearTimeout(timer)
  })
})

// 手动停止监听
const stop = () => {
  stopWatch()
}
</script>

<template>
  <input v-model="inputVal" placeholder="输入搜索内容">
  <button @click="stop">停止监听</button>
</template>
(3)进阶用法 2:执行时机配置(flush)
vue 复制代码
<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)

// 1. 默认(flush: 'sync'):同步执行(数据变化立即触发)
watchEffect(() => {
  console.log('同步执行:', count.value)
})

// 2. flush: 'post':DOM更新后执行(相当于watchPostEffect)
watchEffect(() => {
  // 可获取更新后的DOM节点
  console.log('DOM更新后执行:', document.getElementById('count').innerText)
}, { flush: 'post' })

// 3. flush: 'pre':组件更新前执行(较少用)
watchEffect(() => {
  console.log('组件更新前执行:', count.value)
}, { flush: 'pre' })
</script>

<template>
  <div id="count">{{ count }}</div>
  <button @click="count++">+1</button>
</template>
避坑要点
  • ✅ 回调函数中必须「直接使用响应式数据」(如count.value),否则无法收集依赖;
  • ✅ 清理副作用必须用onInvalidate(不能在回调外清理),确保依赖变化时先清理旧副作用;
  • ❌ 无法获取旧值,如需旧值需用watch
  • ✅ 组件卸载时会自动停止监听,无需手动调用stop(除非需要提前停止)。

4. watchPostEffect /watchSyncEffect:简化执行时机

watchEffect的语法糖,无需配置flush

  • watchPostEffect = watchEffect({ flush: 'post' }):DOM 更新后执行,适合操作更新后的 DOM;
  • watchSyncEffect = watchEffect({ flush: 'sync' }):强制同步执行,适合需要立即响应的场景。
vue 复制代码
<script setup>
import { ref, watchPostEffect, watchSyncEffect } from 'vue'

const count = ref(0)

// DOM更新后执行
watchPostEffect(() => {
  console.log('DOM更新后:', document.getElementById('count').innerText)
})

// 同步执行
watchSyncEffect(() => {
  console.log('同步执行:', count.value)
})
</script>

四、响应式辅助 API:toRef /toRefs/toRaw /unref

解决「响应式数据的属性操作、解构、原始值获取」等问题,是日常开发的 "工具类" API。

1. toRef:单个属性的响应式引用

核心作用

reactive对象的单个属性 创建响应式引用(ref),与原对象保持「引用关联」(修改toRef会同步修改原对象,反之亦然)。

用法示例
vue 复制代码
<script setup>
import { reactive, toRef } from 'vue'

const user = reactive({
  name: '张三',
  age: 18
})

// 为user.name创建响应式引用
const nameRef = toRef(user, 'name')

// 修改nameRef,原对象同步更新
nameRef.value = '李四'
console.log(user.name) // 输出:李四

// 修改原对象,nameRef同步更新
user.name = '王五'
console.log(nameRef.value) // 输出:王五
</script>
适用场景
  • 只需要使用对象的某个属性,且希望保持响应式(无需解构整个对象);

  • 组件传参时,仅传递对象的单个属性(保持响应式关联);

  • 为不存在的属性创建引用(不会报错,后续赋值会新增属性):

    javascript 复制代码
    const genderRef = toRef(user, 'gender') // user.gender不存在,不会报错
    genderRef.value = '男' // user.gender新增为'男',响应式生效

2. toRefs:批量转换响应式属性

核心作用

reactive对象的所有属性 批量转为ref对象,返回一个普通对象,解决「解构reactive对象丢失响应式」的问题。

用法示例
vue 复制代码
<script setup>
import { reactive, toRefs } from 'vue'

const user = reactive({
  name: '张三',
  age: 18,
  address: { city: '北京' }
})

// 批量转换为ref对象
const userRefs = toRefs(user)
// userRefs结构:{ name: Ref('张三'), age: Ref(18), address: Ref(...) }

// 解构后仍保持响应式
const { name, age, address } = userRefs

// 修改时需加 .value
name.value = '李四' // user.name同步更新
address.value.city = '上海' // user.address.city同步更新
</script>
避坑要点
  • toRefs不会创建新的响应式数据,只是对原属性的「引用」;
  • ❌ 原对象新增属性时,toRefs不会自动同步(需手动用toRef添加);
  • ✅ 适合配合解构赋值使用,让代码更简洁(无需每次写user.name)。

3. toRaw:获取响应式对象的原始值

核心作用

获取reactiveref包装后的原始对象 / 值,修改原始值不会触发响应式更新(适合临时修改、性能优化)。

用法示例
vue 复制代码
<script setup>
import { reactive, ref, toRaw } from 'vue'

// 1. 处理reactive对象
const user = reactive({ name: '张三' })
const rawUser = toRaw(user) // 获取原始对象

// 修改原始值:不会触发响应式更新
rawUser.name = '李四'
console.log(user.name) // 输出:李四(原对象也会变,但无响应式触发)

// 2. 处理ref对象(需先访问.value)
const count = ref(0)
const rawCount = toRaw(count.value) // ref需先取.value
rawCount = 10 // 无响应式,视图不更新

// 3. 实战场景:批量修改数据(避免多次触发更新)
const list = reactive([1, 2, 3, 4, 5])
const rawList = toRaw(list)
// 批量修改原始数组,仅触发一次更新(如果直接修改list会触发多次)
rawList.push(6, 7, 8)
</script>
适用场景
  • 批量修改响应式对象(避免多次触发更新,提升性能);
  • 向第三方库传递数据(第三方库不需要响应式,避免 Proxy 包装导致的兼容问题);
  • 临时修改数据(不需要视图更新,如日志打印、数据校验)。

4. unref:简化 ref 值的访问

核心作用

语法糖:如果参数是ref对象,返回value;否则返回参数本身(避免手动判断isRef)。

用法示例
vue 复制代码
<script setup>
import { ref, unref } from 'vue'

const count = ref(0)
const num = 10

// 等价于:const val1 = isRef(count) ? count.value : count
const val1 = unref(count) // 输出:0

// 等价于:const val2 = isRef(num) ? num.value : num
const val2 = unref(num) // 输出:10

// 实战场景:函数参数支持ref和普通值
const add = (a, b) => {
  return unref(a) + unref(b)
}

console.log(add(count, num)) // 0 + 10 = 10
console.log(add(5, num)) // 5 + 10 = 15
</script>

五、数据保护 API:readonly /shallowReadonly

核心作用

创建「只读」的响应式对象,禁止修改属性(保护核心数据不被意外篡改),支持「深层只读」和「浅层只读」。

底层原理

通过 Proxy 拦截set操作,当尝试修改属性时,在开发环境抛出警告,生产环境静默失败(不修改属性)。

用法示例

vue 复制代码
<script setup>
import { reactive, readonly, shallowReadonly } from 'vue'

const user = reactive({
  name: '张三',
  address: {
    city: '北京',
    area: '朝阳区'
  }
})

// 1. 深层只读:所有层级的属性都不能修改
const readOnlyUser = readonly(user)

readOnlyUser.name = '李四' // 开发环境警告:无法修改只读属性
readOnlyUser.address.city = '上海' // 开发环境警告:深层属性也无法修改

// 2. 浅层只读:仅顶层属性不能修改,深层属性可修改
const shallowUser = shallowReadonly(user)

shallowUser.name = '李四' // 开发环境警告:顶层属性不可修改
shallowUser.address.city = '上海' // 成功:深层属性可修改(无警告)

// 3. 只读ref对象(直接用readonly包装ref)
const count = ref(0)
const readOnlyCount = readonly(count)
readOnlyCount.value = 10 // 开发环境警告
</script>
适用场景
  • 传递给子组件的数据,不希望子组件修改(如全局配置、静态数据);
  • 保护接口返回的原始数据(避免意外篡改,如需修改可基于原始数据创建副本);
  • 共享状态(如 Pinia 中的部分状态),只允许通过特定方法修改,不允许直接修改属性。

六、性能优化 API:shallowRef /shallowReactive

核心作用

创建「浅层」响应式对象,仅监听顶层属性变化(深层属性变化不触发响应式),用于优化深层数据结构的性能(避免深层 Proxy 代理的开销)。

核心区别(与 ref/reactive)

API 监听层级 触发更新条件 适用场景
ref 深层 任意层级属性变化 基本类型、需要监听深层变化的复杂类型
shallowRef 浅层 .value替换时 深层数据结构,仅需要替换整个对象
reactive 深层 任意层级属性变化 简单对象,需要监听深层变化
shallowReactive 浅层 仅顶层属性变化 深层数据结构(如大数据列表),仅需要监听顶层属性

用法示例

1. shallowRef:仅监听.value替换
vue 复制代码
<script setup>
import { shallowRef } from 'vue'

// 浅层ref:仅监听.value的替换
const user = shallowRef({
  name: '张三',
  address: { city: '北京' }
})

// ✅ 触发响应式更新(替换整个.value)
user.value = { name: '李四', address: { city: '上海' } }

// ❌ 不触发响应式更新(修改深层属性)
user.value.address.city = '广州'

// 手动触发更新(如需监听深层变化)
import { triggerRef } from 'vue'
const updateDeep = () => {
  user.value.address.city = '广州'
  triggerRef(user) // 手动触发响应式更新
}
</script>
2. shallowReactive:仅监听顶层属性
vue 复制代码
<script setup>
import { shallowReactive } from 'vue'

// 浅层reactive:仅监听顶层属性
const list = shallowReactive([
  { id: 1, name: 'Vue3', detail: { score: 95 } },
  { id: 2, name: 'React', detail: { score: 90 } }
])

// ✅ 触发响应式更新(修改顶层属性)
list[0].name = 'Vue3.4'
list.push({ id: 3, name: 'Node.js' })

// ❌ 不触发响应式更新(修改深层属性)
list[0].detail.score = 98

// 手动触发更新(如需监听深层变化)
import { triggerReactivity } from 'vue'
const updateDeep = () => {
  list[0].detail.score = 98
  triggerReactivity(list[0].detail) // 手动触发
}
</script>
适用场景
  • 大数据列表(如表格数据,1000 + 条记录):仅需要增删改查列表项,不需要监听列表项内部属性变化;
  • 深层嵌套的配置对象:仅需要替换整个配置,不需要监听配置内部的深层属性;
  • 第三方库返回的复杂对象:不需要响应式监听,仅需要偶尔替换整个对象。

七、组件通信 API:provide /inject

核心作用

解决「深层组件通信」问题(如爷孙组件、跨多级组件),无需逐层传递props,实现 "跨层级数据共享"。

底层原理

  • provide:在父组件中注册 "依赖提供者",将数据 / 方法存入当前组件的「依赖注入上下文」;
  • inject:在子组件中从「依赖注入上下文」中获取对应的数据 / 方法,无论层级多深。

分层用法示例

(1)基础用法:传递普通数据
vue 复制代码
<!-- 顶层组件(如App.vue):提供者 -->
<script setup>
import { provide } from 'vue'

// 提供普通数据(非响应式)
provide('appName', 'Vue3 Demo')
provide('version', '3.4.0')
</script>
vue 复制代码
<!-- 深层子组件(如GrandChild.vue):使用者 -->
<script setup>
import { inject } from 'vue'

// 注入数据(第二个参数为默认值)
const appName = inject('appName', '默认名称')
const version = inject('version', '1.0.0')
const author = inject('author', '未知') // 无提供者,使用默认值
</script>

<template>
  <p>应用名称:{{ appName }}</p>
  <p>版本:{{ version }}</p>
  <p>作者:{{ author }}</p>
</template>
(2)进阶用法:传递响应式数据 + 方法
vue 复制代码
<!-- 顶层组件:提供者 -->
<script setup>
import { ref, provide } from 'vue'

// 响应式数据
const theme = ref('light') // 主题:light/dark

// 方法:修改主题
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

// 提供响应式数据和方法
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
vue 复制代码
<!-- 深层子组件:使用者 -->
<script setup>
import { inject } from 'vue'

// 注入响应式数据和方法
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>

<template>
  <div :class="`app theme-${theme.value}`">
    <p>当前主题:{{ theme.value }}</p>
    <button @click="toggleTheme">切换主题</button>
  </div>
</template>

<style>
.theme-light { background: #fff; color: #333; }
.theme-dark { background: #333; color: #fff; }
</style>
(3)高级用法:提供只读数据(避免子组件修改)
vue 复制代码
<!-- 顶层组件:提供者 -->
<script setup>
import { ref, provide, readonly } from 'vue'

const userInfo = ref({ name: '张三', role: 'admin' })

// 提供只读版本的响应式数据
provide('userInfo', readonly(userInfo))

// 提供修改方法(子组件只能通过方法修改)
const updateUserName = (newName) => {
  userInfo.value.name = newName
}
provide('updateUserName', updateUserName)
</script>
vue 复制代码
<!-- 子组件:使用者 -->
<script setup>
import { inject } from 'vue'

const userInfo = inject('userInfo')
const updateUserName = inject('updateUserName')

// 尝试直接修改:开发环境警告(只读)
const tryModify = () => {
  userInfo.value.name = '李四' // 警告:无法修改只读属性
}

// 正确修改:通过提供的方法
const modifyName = () => {
  updateUserName('李四') // 成功:修改生效
}
</script>
避坑要点
  • provideinjectkey必须一致(字符串类型,建议用 Symbol 避免冲突);
  • ✅ 传递响应式数据时,子组件修改会同步到父组件(如需只读,配合readonly);
  • ❌ 不要滥用:仅用于跨层级通信,父子组件通信优先用props+emit
  • ✅ 建议在顶层组件集中管理provide,避免分散在多个组件中(难以维护)。

八、组件工具 API:useAttrs /useSlots

核心作用

<script setup>中获取组件的「非 props 属性(attrs)」和「插槽(slots)」,用于开发通用 UI 组件(如按钮、卡片、表单组件)。

1. useAttrs:获取组件的非 props 属性

用法示例
vue 复制代码
<!-- 子组件:MyButton.vue -->
<script setup>
import { useAttrs } from 'vue'

// 获取所有非props属性(如type、class、style、事件监听等)
const attrs = useAttrs()

// 访问单个属性
console.log(attrs.type) // 如父组件传递type="primary"
console.log(attrs.onClick) // 父组件传递的@click事件
</script>

<template>
  <!-- 透传所有attrs(v-bind="attrs") -->
  <button v-bind="attrs" class="my-button">
    <slot /> <!-- 渲染默认插槽 -->
  </button>
</template>

<style scoped>
.my-button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
}
/* 结合attrs.type自定义样式 */
.my-button[type="primary"] {
  background: #42b983;
  color: #fff;
}
.my-button[type="danger"] {
  background: #f56c6c;
  color: #fff;
}
</style>
vue 复制代码
<!-- 父组件:使用MyButton -->
<template>
  <MyButton type="primary" @click="handleClick" class="custom-class">
    主要按钮
  </MyButton>
  <MyButton type="danger" @click="handleDelete">
    危险按钮
  </MyButton>
</template>

<script setup>
const handleClick = () => console.log('点击主要按钮')
const handleDelete = () => console.log('点击危险按钮')
</script>
关键说明
  • attrs包含:非 props 属性、原生事件监听(如onClick)、classstyle
  • 透传attrs时,v-bind="attrs"会自动将属性和事件绑定到元素上;
  • 如需排除某些属性,可手动解构attrsconst { type, ...restAttrs } = attrs

2. useSlots:获取组件的插槽

用法示例
vue 复制代码
<!-- 子组件:MyCard.vue -->
<script setup>
import { useSlots } from 'vue'

// 获取所有插槽
const slots = useSlots()

// 检查是否存在某个插槽
console.log(slots.default) // 默认插槽(存在则为函数)
console.log(slots.header) // 具名插槽header(存在则为函数)
console.log(slots.footer) // 具名插槽footer(不存在则为undefined)
</script>

<template>
  <div class="card">
    <!-- 渲染具名插槽header(如有) -->
    <div class="card-header" v-if="slots.header">
      <slot name="header" />
    </div>
    <!-- 渲染默认插槽 -->
    <div class="card-body">
      <slot />
    </div>
    <!-- 渲染具名插槽footer(如有) -->
    <div class="card-footer" v-if="slots.footer">
      <slot name="footer" />
    </div>
  </div>
</template>

<style scoped>
.card {
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 16px;
}
.card-header {
  font-size: 18px;
  font-weight: bold;
  margin-bottom: 8px;
}
.card-footer {
  margin-top: 16px;
  color: #666;
}
</style>
vue 复制代码
<!-- 父组件:使用MyCard -->
<template>
  <MyCard>
    <!-- 具名插槽header -->
    <template #header>
      <h3>卡片标题</h3>
    </template>
    <!-- 默认插槽 -->
    <p>卡片内容:这是一个基于slot的通用卡片组件</p>
    <!-- 具名插槽footer -->
    <template #footer>
      <button @click="handleClose">关闭</button>
    </template>
  </MyCard>
</template>

<script setup>
const handleClose = () => console.log('关闭卡片')
</script>
适用场景
  • 开发通用 UI 组件库(如按钮、卡片、表单、对话框);
  • 需要支持自定义内容(插槽)和自定义属性 / 事件(attrs)的组件;
  • 组件透传场景(如将父组件的属性 / 事件透传给子组件的内部元素)。

九、常用生命周期钩子(组合式 API 版)

组合式 API 的生命周期钩子需要「按需导入」,名称以on开头,与 Vue2 的对应关系如下:

Vue2 选项式 API Vue3 组合式 API 作用 适用场景
beforeCreate -(setup 中直接执行) 组件创建前 无(setup 执行时机等同于 beforeCreate+created)
created -(setup 中直接执行) 组件创建后 初始化数据、请求接口(无需等待 DOM)
beforeMount onBeforeMount 组件挂载前 准备 DOM 相关操作(如设置初始 DOM 属性)
mounted onMounted 组件挂载后 DOM 操作、初始化第三方库(如图表、地图)
beforeUpdate onBeforeUpdate 组件更新前 保存 DOM 更新前的状态(如滚动位置)
updated onUpdated 组件更新后 同步第三方库状态(如图表数据更新)
beforeUnmount onBeforeUnmount 组件卸载前 清理资源(定时器、事件监听、接口请求)
unmounted onUnmounted 组件卸载后 最终清理(如销毁第三方库实例)
errorCaptured onErrorCaptured 捕获子组件错误 全局错误处理、错误日志上报

用法示例

vue 复制代码
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'

const count = ref(0)
let timer = null

// 组件挂载后执行(DOM已渲染)
onMounted(() => {
  console.log('组件挂载完成:', document.getElementById('count').innerText)
  // 初始化定时器
  timer = setInterval(() => {
    count.value++
  }, 1000)
})

// 组件更新后执行
onUpdated(() => {
  console.log('组件更新完成:', count.value)
})

// 组件卸载前执行(清理资源)
onUnmounted(() => {
  console.log('组件即将卸载')
  clearInterval(timer) // 清理定时器
  // 取消接口请求、解绑事件监听等
})
</script>

<template>
  <div id="count">{{ count }}</div>
</template>

总结:常用组合式 API 核心要点回顾

1. 响应式基础

  • ref:基本类型 + 兼容复杂类型,需.value,支持替换整个对象;
  • reactive:仅复杂类型,无需.value,不支持替换整个对象;
  • 选择原则:基本类型用ref,简单对象用reactive,需解构 / 替换用ref

2. 响应式派生与监听

  • computed:缓存派生值,支持只读 / 可写,避免副作用;
  • watch:精准监听,支持旧值 / 多源 / 深度监听,适合需要明确控制的场景;
  • watchEffect:自动收集依赖,简洁高效,适合无需旧值的场景;
  • watchPostEffect/watchSyncEffect:控制执行时机,简化配置。

3. 辅助与工具 API

  • toRef/toRefs:解决reactive解构丢响应式;
  • toRaw:获取原始值,优化批量修改;
  • readonly/shallowReadonly:保护数据不被篡改;
  • shallowRef/shallowReactive:优化深层数据性能。

4. 组件通信与工具

  • provide/inject:跨层级通信,配合readonly保证数据安全;
  • useAttrs/useSlots:开发通用 UI 组件,支持属性透传和插槽自定义。

5. 生命周期

  • 核心钩子:onMounted(DOM 操作)、onUnmounted(清理资源)、onUpdated(同步状态);
  • setup 执行时机:等同于beforeCreate+created,无需额外钩子。

学习建议

  1. 先掌握核心 API:ref/reactivecomputedwatch/watchEffect→生命周期;
  2. 再学习辅助 API:toRefs/toRawreadonlyshallow系列;
  3. 最后实战场景:provide/injectuseAttrs/useSlots
  4. 多写案例(如 TodoList、表单联动、通用组件),结合实际场景理解 API 用途。

这些 API 覆盖了 Vue3 项目 95% 以上的开发场景,熟练掌握后,无论是业务开发还是组件封装,都能游刃有余!如果遇到具体场景不确定用哪个 API,可对照上述 "适用场景" 快速选择~

相关推荐
秋邱1 小时前
AR + 离线 AI 实战:YOLOv9+TensorFlow Lite 实现移动端垃圾分类识别
开发语言·前端·数据库·人工智能·python·html
蜡笔小嘟1 小时前
使用gemini 3 pro实现可视化大屏
前端·ai·gemini·gemini3peo
马玉霞1 小时前
vue3很丝滑的table表格向上滚动效果,多用于统计页面
前端·vue.js
用户952081611791 小时前
百度地图JSAPI THREE Label 组件使用指南,轻松实现地图标签渲染
前端
SVIP111591 小时前
webpack入门 精细版
前端·webpack·node.js
畅畅畅哥哥1 小时前
Next.js App Router 实战避坑:状态、缓存与测试
前端·前端框架
一水鉴天1 小时前
整体设计 定稿 之20 拼语言表述体系之3 dashboard.html完整代码
java·前端·javascript
一颗烂土豆1 小时前
React 大屏可视化适配方案:vfit-react 发布 🚀
前端·javascript·react.js
Qinana1 小时前
构建一个融合前端、模拟后端与大模型服务的全栈 AI 应用
前端·后端·程序员