1863 字
9 分钟
Vue 3 Ref 响应式原理深度解析
为什么需要包装原始值?
JavaScript 响应式的根本问题
JavaScript 语言层面的限制导致了原始值无法实现响应式:
// ❌ 原始值无法实现响应式let count = 0count = 10 // 没有任何机制能拦截这个赋值操作而对象可以通过多种方式实现响应式:
// ✅ 对象可以通过各种方式监听属性变化let obj = { count: 0 }
Object.defineProperty(obj, 'count', { get() { console.log('读取count') }, set(val) { console.log('设置count为:', val) }})
// 方式2:Proxyconst 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) // ❌ undefinedfor (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 实现响应式 - 设计目标:统一原始值和对象的响应式处理方式
关键设计特点
- 单一属性设计:所有依赖都收集在 ‘value’ 属性上,简化了依赖管理
- 类型转换:如果包装的是对象,内部自动用
reactive()处理 - 标识字段:
__v_isRef用于识别 ref 对象,避免重复包装
重要理解
- ref 解决了 JavaScript 无法拦截原始值赋值的根本限制
- 通过对象包装,将值访问转换为属性访问,从而可以被拦截
- 这是响应式系统的基础设计,性能开销换取了强大的响应式能力
这种设计使得 Vue 3 能够以统一的方式处理各种类型的响应式数据,是整个响应式系统的核心基础。
Vue 3 Ref 响应式原理深度解析
https://fuwari.vercel.app/posts/vue-ref/