Vue3 核心 API 深度解析:ref / reactive / computed / watch

前言:很多人学 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 三种监听源

  1. ref
  2. reactive 对象
  3. 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>

七、终极总结(高级前端必背)

  1. ref 是基础类型的响应式外壳,内部可自动转 reactive
  2. reactive 是 Proxy,只能代理对象,不能直接赋值替换
  3. computed 是惰性缓存求值,禁止副作用与异步
  4. watch 是副作用监听,适合请求、DOM 操作、日志
  5. 复杂状态用 reactive,简单状态用 ref
  6. 派生状态用 computed,变化逻辑用 watch
  7. 90% 的响应式 bug 都来自:解构、赋值覆盖、不懂 Proxy 限制、不理解依赖追踪
相关推荐
console.log('npc')2 小时前
partial在react接口定义中是什么意思
前端·javascript·typescript
SuperEugene2 小时前
前端 utils 工具函数规范:拆分 / 命名 / 复用全指南,避开全局污染等高频坑|编码语法规范篇
开发语言·前端·javascript
C澒2 小时前
微前端容器标准化 —— 公共能力篇:通用请求
前端·架构
llxxyy卢2 小时前
web部分中等题目
android·前端
若惜2 小时前
selenium自动化测试web自动化测试 框架封装Pom
前端·python·selenium
Amumu121382 小时前
Js:内置对象
开发语言·前端·javascript
广州华水科技2 小时前
2026年单北斗GNSS变形监测系统推荐,助力精准监控与智慧城市建设
前端
鸡吃丸子2 小时前
如何编写一个高质量的AI Skill
前端·ai
我命由我123452 小时前
Element Plus 2.2.27 的单选框 Radio 组件,选中一个选项后,全部选项都变为选中状态
开发语言·前端·javascript·html·ecmascript·html5·js