Vue3.0最核心的变化之一就是重写了响应性系统。过去Vue2依赖Object.defineProperty对属性做劫持,存在无法监听新增属性、数组下标赋值不触发更新等先天缺陷。Vue3改用ES6的Proxy来代理整个对象,从根上解决了这些问题。但新的API也带来了新的坑,比如reactive对象被解构后失效、ref忘记加.value等。这篇文章把Vue3响应性基础的原理、用法和常见误区一次讲清楚。

Vue3响应性的底层原理:Proxy如何拦截一切
理解响应性,先要明白它要解决什么问题:当某个数据变化时,所有依赖这个数据的代码(通常是模板渲染函数)能够自动重新执行。要实现这一点,框架必须知道两件事:谁在读数据、谁在写数据。Proxy恰好提供了这种能力。
Vue3在创建响应式对象时,会用reactive函数把原始对象包一层Proxy。当代码读取属性时,Proxy的get拦截器触发,Vue记录下当前正在运行的副作用函数与这个属性的关系(依赖收集);当属性被赋值时,set拦截器触发,Vue找到所有依赖该属性的副作用函数并重新执行(派发更新)。来看一个简化版实现:
let activeEffect = null
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, (deps = new Set()))
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (deps) deps.forEach(fn => fn())
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return Reflect.get(target, key)
},
set(target, key, value) {
Reflect.set(target, key, value)
trigger(target, key)
return true
}
})
}
这段代码虽然省略了大量边界处理,但核心流程与Vue3源码一致:get时track收集依赖,set时trigger触发更新。相比Vue2的defineProperty,Proxy能拦截的操作多得多,包括属性新增删除、数组索引赋值、遍历等,所以Vue3里直接obj.newProp = 1或arr[0] = 1都能正常触发更新,不再需要$set。
另外一个值得注意的细节是嵌套对象的惰性代理。Vue3不会在初始化时递归代理所有子对象,而是在get到某个子对象时才将其转换为代理,这样做能显著减少初始化开销,尤其在处理大型深层对象时性能更优。
ref与reactive:两个核心API的正确用法
reactive接收一个对象并返回其代理版本,它有两个天然限制:只能用于对象类型(对象、数组、Map、Set等),且不能整体替换。而ref是为任意类型设计的,包括number、string、boolean这些基本类型。基本类型无法被Proxy代理,所以ref采用的方式是把值装进一个带value属性的对象里,通过getter和setter拦截value的读写:
import { ref, reactive } from 'vue'
// 基本类型用ref
const count = ref(0)
count.value++ // 必须通过.value访问
// 对象可以用reactive
const user = reactive({ name: '张三', age: 25 })
user.age = 26 // 直接访问属性,无需.value
// ref也能装对象,内部会自动调用reactive
const info = ref({ score: 90 })
info.value.score = 100
两者的选择建议可以这样把握:单一独立值(计数器、开关、输入框文本)优先用ref;一组相关联的状态组成实体(表单数据、用户信息)可以用reactive。团队协作时最好统一规范,避免ref和reactive混用导致心智负担。
有一个容易忽略的点:ref在模板中使用时不需要写.value,因为模板编译后的渲染函数会自动解包。但在JS逻辑代码中忘记.value是新手最高频的错误之一。反过来,reactive对象解构后也会失去响应性,这点在下文的误区部分详细展开。
Vue还提供了几个配套工具函数:isRef判断是否为ref对象,unref安全取值,readonly创建只读代理,shallowRef和shallowReactive创建浅层响应式对象。浅层版本只代理第一层,对于不需要深层追踪的大数据结构能减少性能损耗。
常见误区逐一排查,看看你踩过几个
误区一:直接解构reactive对象
这是最常见的坑。解构操作拿到的是纯值,而不是代理后的引用:
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'test' })
// 错误:解构后count和name变成普通值,失去响应性
let { count, name } = state
// 正确:用toRefs把每个属性转成ref
const { count, name } = toRefs(state)
// 使用时通过.value访问
console.log(count.value)
注意区分toRefs和toRef:前者转换整个对象的所有属性,后者只转单个属性。从组合式函数返回reactive状态时,惯例做法是返回toRefs后的对象,这样调用方解构也不会丢响应性。
误区二:直接替换reactive变量整体
reactive声明的变量不能整体赋值,因为一旦替换,变量名指向的就是新的普通对象,代理关系断了:
const list = reactive([1, 2, 3]) // 错误:整体替换后失去响应 // list = [4, 5, 6] // 正确:修改内容而不是替换引用 list.splice(0, list.length, 4, 5, 6) list.push(7) // 追加元素正常 // 或者干脆一开始就用ref装数组 const listRef = ref([1, 2, 3]) listRef.value = [4, 5, 6] // 整体替换没问题
这也是很多团队倾向全部使用ref的原因:ref可以随意整体替换,自由度更高。
误区三:误以为ref嵌套在reactive里需要.value
当一个ref作为reactive对象的属性时,访问它会自动解包,不需要再.value:
const count = ref(0)
const state = reactive({ count })
state.count++ // 自动解包,等价于count.value++
// 但注意:新的数组或Map中的ref不会自动解包
const arr = reactive([ref(1)])
// arr[0] 是ref对象本身,需要arr[0].value
误区四:watch监听reactive对象时的行为差异
直接传reactive对象给watch会隐式创建深层监听,而传一个返回reactive对象的getter函数则只监听引用变化:
const state = reactive({ nested: { value: 1 } })
// 隐式深层监听,nested内部变化也会触发
watch(state, () => { console.log('changed') })
// 显式指定deep选项
watch(() => state, callback, { deep: true })
// 监听具体属性,注意返回的是值,若属性是对象需要deep
watch(() => state.nested, callback, { deep: true })
误区五:副作用中修改自身导致死循环
在watchEffect或watch回调里修改被监听的数据,可能造成无限循环。虽然Vue对同一tick内的重复触发做了次数上限保护(超过100次会警告),但仍应从逻辑上避免这种自触发的设计。
实战建议与性能优化
日常开发中可以沉淀几条经验。第一,组件内单一状态用ref,逻辑聚合度高的状态对象用reactive加toRefs。第二,大列表数据如果只是展示不需要逐项追踪,用shallowRef配合手动triggerRef触发更新,避免为成千上万条数据建立依赖关系。第三,从接口拿到的临时数据如果只用于渲染一次,可以用markRaw标记跳过代理转换,减少开销:
import { markRaw, shallowRef, triggerRef } from 'vue'
import Chart from 'chart-library'
// 第三方类实例不需要响应式,避免被代理
const chart = shallowRef(markRaw(new Chart()))
// 大数据量场景:浅层ref加手动触发
const bigList = shallowRef([])
bigList.value = fetchHugeList()
triggerRef(bigList) // 手动通知更新
响应性系统是Vue3一切上层能力的地基,组合式API、computed、watch、乃至Pinia的状态管理都构建在这套Proxy机制之上。把本文的原理和误区消化掉,遇到视图不更新这类问题时,你就能顺着track和trigger的链路去定位,而不是盲目地改代码碰运气。建议动手把上面的简化版reactive实现自己敲一遍,理解会深刻得多。
Vue3.0响应性Vue3响应式原理ref与reactive修改时间:2026-09-16 00:09:44