1863 字
9 分钟
Vue 3 Ref 响应式原理深度解析

为什么需要包装原始值?#

JavaScript 响应式的根本问题#

JavaScript 语言层面的限制导致了原始值无法实现响应式:

// ❌ 原始值无法实现响应式
let count = 0
count = 10 // 没有任何机制能拦截这个赋值操作

而对象可以通过多种方式实现响应式:

Object.defineProperty
// ✅ 对象可以通过各种方式监听属性变化
let obj = { count: 0 }
Object.defineProperty(obj, 'count', {
get() { console.log('读取count') },
set(val) { console.log('设置count为:', val) }
})
// 方式2:Proxy
const proxyObj = new Proxy(obj, {
get(target, key) { console.log('读取', key) },
set(target, key, val) { console.log('设置', key, '为', val) }
})

核心问题:JavaScript 只能拦截对象的属性访问,无法拦截原始值的重新赋值。

RefImpl 类的内部实现#

Vue 3 通过 RefImpl 类来解决这个问题:

class RefImpl {
constructor(value, shallow = false) {
this._value = value
this.__v_isRef = true // 标识这是一个ref对象
this.__v_isShallow = shallow // 是否是浅响应式
this._rawValue = value // 保存原始值用于比较
// 如果不是浅响应式且值是对象,则用reactive包装
this._value = shallow ? value : convert(value)
// 创建依赖收集器
this.dep = new Set()
}
get value() {
// 依赖收集:将当前effect添加到dep中
track(this, 'value')
return this._value
}
set value(newValue) {
// 值比较:避免不必要的更新
if (hasChanged(newValue, this._rawValue)) {
this._rawValue = newValue
this._value = this.__v_isShallow ? newValue : convert(newValue)
// 触发更新:通知所有依赖此ref的effect
trigger(this, 'value', newValue)
}
}
}

相关辅助函数#

// 类型转换函数
const convert = (val) => isObject(val) ? reactive(val) : val
// 值比较函数
const hasChanged = (value, oldValue) => {
return !Object.is(value, oldValue)
}
// ref函数
function ref(value) {
// 如果已经是ref对象,直接返回
if (isRef(value)) {
return value
}
return new RefImpl(value)
}
// 判断是否是ref对象
function isRef(r) {
return !!(r && r.__v_isRef === true)
}

包装过程示例#

原始值包装#

const count = ref(10)
// 实际创建的对象结构:
/*
RefImpl {
_value: 10, // 当前值
_rawValue: 10, // 原始值(用于比较)
__v_isRef: true, // ref标识
__v_isShallow: false, // 深度响应式
dep: Set(), // 依赖收集器
get value() { ... }, // getter:依赖收集
set value(val) { ... } // setter:触发更新
}
*/

对象值的处理#

const user = ref({ name: 'vue', age: 3 })
// 实际创建的对象结构:
/*
RefImpl {
_value: Proxy({ name: 'vue', age: 3 }), // 对象被reactive包装
_rawValue: { name: 'vue', age: 3 }, // 原始对象
__v_isRef: true,
dep: Set(),
// ...
}
*/

包装机制的核心价值#

1. 统一的响应式接口#

function createReactiveValue(value) {
if (typeof value === 'object') {
return reactive(value) // 对象用Proxy
} else {
return ref(value) // 原始值用包装对象
}
}

2. 依赖收集的统一化#

Vue 3 使用 targetMap 作为全局的依赖收集映射,其数据结构如下:

// 全局依赖收集映射
const targetMap = new WeakMap()
// 数据结构层次:
// WeakMap {
// target1: Map { // depsMap - 每个响应式对象的依赖映射
// 'property1': Set([effect1, effect2]), // 该属性的所有依赖effect
// 'property2': Set([effect3, effect4]),
// 'value': Set([effect5]) // ref对象固定使用'value'作为key
// },
// target2: Map { ... }
// }
function track(target, key) {
if (!activeEffect) return
// 1. 从全局targetMap中获取当前对象的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
// 2. 如果该对象还没有依赖映射,创建一个新的Map
targetMap.set(target, (depsMap = new Map()))
// 注意:(depsMap = new Map()) 赋值表达式的返回值是新创建的Map实例
}
// 3. 获取该属性的依赖集合
let dep = depsMap.get(key) // ref的key固定是'value'
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 4. 将当前effect添加到依赖集合中
dep.add(activeEffect)
}

为什么使用 WeakMap?#

// WeakMap的三大优势:
// 1. 弱引用特性 - 防止内存泄漏
let obj = reactive({ count: 0 })
// targetMap会为obj创建依赖映射
obj = null // 当obj被垃圾回收时,targetMap中的条目也会自动被移除
// 2. 只能用对象作为key - 保证类型安全
targetMap.set('string', new Map()) // ❌ 报错,key必须是对象
targetMap.set(refObj, new Map()) // ✅ 正确
// 3. 无法遍历 - 避免意外访问内部状态
console.log(targetMap.size) // ❌ undefined
for (let item of targetMap) {} // ❌ 不可遍历

完整的依赖收集示例#

// 示例:computed依赖收集过程
const count = ref(0)
const doubled = computed(() => count.value * 2)
// 当访问doubled.value时的内部流程:
function computedGetter() {
activeEffect = computedEffect // 设置当前活跃effect
// 执行getter: () => count.value * 2
// 访问count.value触发track(countRef, 'value')
// track执行过程:
let depsMap = targetMap.get(countRef) // undefined(首次)
if (!depsMap) {
targetMap.set(countRef, (depsMap = new Map()))
}
let dep = depsMap.get('value') // undefined(首次)
if (!dep) {
depsMap.set('value', (dep = new Set()))
}
dep.add(computedEffect) // 添加计算属性的effect
// 最终targetMap结构:
// WeakMap {
// countRef => Map {
// 'value' => Set([computedEffect])
// }
// }
}

3. 更新触发的统一化#

function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
if (effects) {
effects.forEach(effect => effect())
}
}

ref 包装的性能影响#

性能开销分析#

function performanceComparison() {
const iterations = 1000000
// 1. 普通变量访问
let primitiveValue = 42
console.time('primitive access')
for (let i = 0; i < iterations; i++) {
const val = primitiveValue // 直接内存访问,最快
}
console.timeEnd('primitive access') // ~1-2ms
// 2. ref包装后访问
const refValue = ref(42)
console.time('ref access')
for (let i = 0; i < iterations; i++) {
const val = refValue.value // 需要调用getter,触发依赖收集
}
console.timeEnd('ref access') // ~50-100ms
// 3. 不在effect中访问ref(无依赖收集)
console.time('ref access (no tracking)')
activeEffect = null // 禁用依赖收集
for (let i = 0; i < iterations; i++) {
const val = refValue.value // 只是简单的属性访问
}
console.timeEnd('ref access (no tracking)') // ~10-20ms
}

内存和性能代价对比#

const memoryUsage = {
// 原始值:8字节(64位数字)
primitive: 42,
// ref包装:~200字节
refWrapped: ref(42),
/*
RefImpl实例: ~100字节
- _value: 8字节
- _rawValue: 8字节
- __v_isRef: 1字节
- __v_isShallow: 1字节
- dep: Set实例 ~80字节
WeakMap/Map条目: ~100字节
- targetMap存储
- depsMap存储
*/
}

性能开销总结:

  • 创建开销:ref() 比直接赋值慢 ~10倍
  • 访问开销:.value 比直接访问慢 ~5-50倍(取决于是否有依赖收集)
  • 更新开销:setter 比直接赋值慢 ~20倍(包含依赖触发)

为什么值得这个代价?#

虽然有性能开销,但换来了强大的响应式能力:

const count = ref(0)
// 1. 自动依赖追踪
const doubled = computed(() => count.value * 2) // 自动更新
// 2. 副作用管理
watchEffect(() => {
document.title = `Count: ${count.value}` // 自动执行
})
// 3. 组件自动更新
// 当count.value改变时,所有使用它的组件自动重新渲染

总结#

包装的本质#

  • 核心问题解决:将不可拦截的原始值变成可拦截的对象属性访问
  • 实现方式:通过 RefImpl.prototype.value 的 getter/setter 实现响应式
  • 设计目标:统一原始值和对象的响应式处理方式

关键设计特点#

  1. 单一属性设计:所有依赖都收集在 ‘value’ 属性上,简化了依赖管理
  2. 类型转换:如果包装的是对象,内部自动用 reactive() 处理
  3. 标识字段__v_isRef 用于识别 ref 对象,避免重复包装

重要理解#

  • ref 解决了 JavaScript 无法拦截原始值赋值的根本限制
  • 通过对象包装,将值访问转换为属性访问,从而可以被拦截
  • 这是响应式系统的基础设计,性能开销换取了强大的响应式能力

这种设计使得 Vue 3 能够以统一的方式处理各种类型的响应式数据,是整个响应式系统的核心基础。

Vue 3 Ref 响应式原理深度解析
https://fuwari.vercel.app/posts/vue-ref/
作者
Lorem Ipsum
发布于
2025-09-10
许可协议
CC BY-NC-SA 4.0