一、响应式基础 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内部维护了一个「依赖追踪 + 缓存机制」:
- 首次访问
computed值时,执行计算函数,收集依赖(如count); - 依赖数据变化时,标记
computed为 "脏值",下次访问时重新计算; - 依赖不变时,直接返回缓存的结果,提升性能。
分层用法示例
(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>
适用场景
-
只需要使用对象的某个属性,且希望保持响应式(无需解构整个对象);
-
组件传参时,仅传递对象的单个属性(保持响应式关联);
-
为不存在的属性创建引用(不会报错,后续赋值会新增属性):
javascriptconst 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:获取响应式对象的原始值
核心作用
获取reactive或ref包装后的原始对象 / 值,修改原始值不会触发响应式更新(适合临时修改、性能优化)。
用法示例
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>
避坑要点
- ✅
provide和inject的key必须一致(字符串类型,建议用 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)、class、style;- 透传
attrs时,v-bind="attrs"会自动将属性和事件绑定到元素上; - 如需排除某些属性,可手动解构
attrs:const { 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,无需额外钩子。
学习建议
- 先掌握核心 API:
ref/reactive→computed→watch/watchEffect→生命周期; - 再学习辅助 API:
toRefs/toRaw→readonly→shallow系列; - 最后实战场景:
provide/inject→useAttrs/useSlots; - 多写案例(如 TodoList、表单联动、通用组件),结合实际场景理解 API 用途。
这些 API 覆盖了 Vue3 项目 95% 以上的开发场景,熟练掌握后,无论是业务开发还是组件封装,都能游刃有余!如果遇到具体场景不确定用哪个 API,可对照上述 "适用场景" 快速选择~