前言:很多人学 Vue3 只停留在
ref(0)、reactive({})的表面用法,却不知道为什么要这样设计、底层如何运作、什么场景会丢失响应式、computed 缓存机制是什么、watch 到底有几种监听策略。本文从原理 + 源码思路 + 陷阱 + 性能 + 最佳实践五个维度,深度剖析 Vue3 四大核心 API,让你真正从"会用"进阶到"懂原理、能避坑、写高性能代码"。
一、ref:不仅仅是"基本类型响应式"
1.1 ref 的本质:包装对象 + 类响应式
很多人以为 ref 只是给数字/字符串用的,其实完全不对。
ref 的本质是:
ts
class RefImpl {
public value; // 核心
private __v_isRef = true; // 标识
constructor(value) {
// 如果是对象 → 自动转为 reactive
this.value = isObject(value) ? reactive(value) : value;
}
}
所以:
- ref 可以包任意类型
- ref 包对象 → 内部自动走 reactive
- ref 包基本类型 → 直接存值
这就是为什么:
js
const a = ref({ name: 'aaa' })
a.value.name = 'bbb' // 依然响应
1.2 为什么 JS 里要写 .value,模板不用?
因为 Vue 编译模板时,自动脱 ref:
html
{{ count }}
↓ 编译后
__unref(count)
而 <script setup> 中没有自动脱 ref,所以必须写 .value。
1.3 ref 陷阱(90% 开发者踩过)
陷阱 1:解构 ref 会丢失响应式
js
const count = ref(0)
const { value } = count // ❌ 普通变量,不再响应
陷阱 2:reactive 包 ref 会自动脱 ref
js
const count = ref(0)
const state = reactive({ count })
state.count // 0 → 自动脱 ref
state.count = 1 // 直接修改,不用 .value
陷阱 3:ref 赋值为新对象不会破坏响应式
js
const obj = ref({ a: 1 })
obj.value = { a: 2 } // ✅ 依然响应
这和 reactive 完全不同。
1.4 最佳实践
- 基本类型 → 必用 ref
- 表单输入、独立状态 → ref
- DOM 引用 → ref
- 数组/对象复杂结构 → 不要用 ref,用 reactive
二、reactive:Proxy 响应式核心与限制
2.1 reactive 本质:Proxy 代理
js
const state = reactive({ a: 1 })
等价于:
js
const proxy = new Proxy(target, {
get(target, key) {
track(target, key); // 收集依赖
return Reflect.get(...)
},
set(target, key, val) {
Reflect.set(...)
trigger(target, key) // 触发更新
}
})
2.2 reactive 最大限制:只能是对象类型
因为 Proxy 不支持基本类型拦截 。
这就是为什么 Vue 要设计 ref 来补全。
2.3 reactive 三大深坑
坑 1:直接赋值会破坏代理
js
const state = reactive({ a: 1 })
state = { a: 2 } // ❌ 破坏代理,不再响应
坑 2:解构会丢失响应式
js
const { a } = state // ❌ 丢失响应
解决方案:
js
const { a } = toRefs(state) // ✅ 保留响应
坑 3:reactive 无法正确监听 "替换整个数组"
js
const list = reactive([1,2,3])
list = [4,5,6] // ❌ 破坏代理
正确写法:
js
list.splice(0, list.length, ...[4,5,6])
或用 ref([])。
2.4 最佳实践
- 复杂对象、表单数据、状态集合 → reactive
- 不要整体赋值,只修改属性
- 需要解构 → 配合 toRefs / toRef
- 数组频繁替换 → 用 ref 而不是 reactive
三、computed:缓存、惰性求值、源码级设计
3.1 computed 不是"自动计算",而是 惰性求值 + 缓存
computed 内部是一个:
Dep依赖dirty标记- 缓存结果的
value
只有依赖变化时,dirty = true,才会重新计算。
3.2 computed vs method 本质区别
- method:每次渲染都执行
- computed:缓存,依赖不变直接返回旧值
性能差距极大,尤其大数据量渲染。
3.3 computed 陷阱
陷阱 1:getter 中写异步代码
js
const a = computed(async () => {
return await fetch(...)
})
❌ 永远不会响应,返回 Promise。
陷阱 2:getter 中修改其他响应式数据
js
const a = computed(() => {
count.value++ // ❌ 副作用,导致无限更新
return count.value
})
陷阱 3:可写 computed 滥用
可写 computed 适合:
- 数据拆分/合并(如全名)
- 双向绑定封装
不适合:
- 复杂逻辑
- 异步
- 多状态联动
3.4 高级用法:计算属性依赖追踪
你可以监听 computed:
js
watch(fullName, () => {})
computed 也可以依赖其他 computed,形成依赖链。
四、watch:深度解析监听机制、执行时机、性能
4.1 watch 三种监听源
- ref
- reactive 对象
- getter 函数(最推荐、最稳定)
js
watch(
() => user.info.age, // ✅ 精准监听
() => {}
)
4.2 watch 核心配置深度解析
deep: true
- 只对对象生效
- 递归遍历所有 key 建立监听
- 性能开销大
immediate: true
- 初始化立即执行一次
- 常用于加载初始数据
flush: 'pre' | 'post' | 'sync'
pre(默认):DOM 更新前执行post:DOM 更新后执行sync:同步触发(极少用)
4.3 watch 与 watchEffect 区别(高频面试题)
- watch:显式指定依赖
- watchEffect:自动收集依赖
watchEffect 更简洁,但:
- 无法获取旧值
- 依赖过多时难以追踪
- 容易造成不必要更新
4.4 watch 深坑
坑 1:监听 reactive 时 oldValue === newValue
因为是引用类型,Proxy 不克隆对象。
解决方案:
js
watch(
() => ({ ...state }),
() => {}
)
坑 2:监听数组时,直接 push/pop 不会触发 deep
js
watch(list, () => {})
list.push(1) // ✅ 能监听到
但替换数组不行,必须用 ref。
坑 3:flush: 'post' 导致获取 DOM 时机错误
很多人在 watch 里操作 DOM 报错,就是因为没加 flush: 'post'。
五、ref / reactive / computed / watch 综合对比(深度总结)
| 特性 | ref | reactive | computed | watch |
|---|---|---|---|---|
| 包装形式 | RefImpl | Proxy | ComputedRef | 副作用函数 |
| 支持类型 | 全部 | 对象/数组 | 派生值 | 任意响应式 |
| 是否缓存 | 否 | 否 | 是 | 否 |
| 可异步 | 否 | 否 | 否 | 是 |
| 解构风险 | 不会 | 会 | 不会 | 不会 |
| 性能 | 优 | 良 | 极优 | 中 |
| 适用场景 | 独立状态、DOM | 复杂对象 | 派生数据 | 变化执行逻辑 |
六、真实项目综合实战(高级写法)
vue
<script setup>
import { ref, reactive, computed, watch } from 'vue'
// 表单模型
const form = reactive({
username: '',
password: ''
})
// 校验规则(computed 缓存)
const isUsernameValid = computed(() => {
return form.username.length >= 4
})
const canSubmit = computed(() => {
return isUsernameValid.value && form.password.length >= 6
})
// 监听提交按钮可用性
watch(canSubmit, (val) => {
console.log('可提交:', val)
}, { flush: 'post' })
// 提交逻辑
const submit = () => {
if (!canSubmit.value) return
// api.submit(form)
}
</script>
七、终极总结(高级前端必背)
- ref 是基础类型的响应式外壳,内部可自动转 reactive
- reactive 是 Proxy,只能代理对象,不能直接赋值替换
- computed 是惰性缓存求值,禁止副作用与异步
- watch 是副作用监听,适合请求、DOM 操作、日志
- 复杂状态用 reactive,简单状态用 ref
- 派生状态用 computed,变化逻辑用 watch
- 90% 的响应式 bug 都来自:解构、赋值覆盖、不懂 Proxy 限制、不理解依赖追踪