Appearance
ref 与 shallowRef:原始值是如何实现响应式的
在上一节中,我们了解到 reactive 函数使用 Proxy 来实现对象的响应式。但 Proxy 只能代理对象类型(object、array),不能代理 string、number、boolean 这样的原始值。
那么,Vue 3 是如何让我们手中的 count = 0 这种原始值也实现响应式的呢?
答案是:将原始值“包装”到一个对象中,并通过该对象的 .value 属性来访问和修改,从而实现拦截。
这个“包装对象”,就是 ref。
RefImpl:ref 的内部实现
当你调用 ref(0) 时,Vue 在内部创建了一个 RefImpl 类的实例。这是一个专用于“包装”值的类:
typescript
// core/packages/reactivity/src/ref.ts
// 这是一个简化的 RefImpl 类
class RefImpl<T> {
// 盒子里的“原始金币”
private _rawValue: T
// 盒子里的“响应式金币”
// (如果 T 是对象,这里会是 T 的 reactive 代理)
private _value: T
// 每个盒子都自带一个“依赖收集器” (Dep)
// 就像一个“订阅了此盒子”的列表
public dep: Dep = new Dep()
// 盒子的“身份证”
public readonly [ReactiveFlags.IS_REF] = true
constructor(value: T, isShallow: boolean) {
this._rawValue = value
// 关键!如果放进盒子的是个对象,
// ref 会自动用 toReactive (即 reactive()) 把它变成响应式
this._value = isShallow ? value : toReactive(value)
}
}RefImpl 的设计有两个关键点:
- 自带
dep:每个ref都是一个独立的“响应式单元”,它自己负责收集(track)和触发(trigger)依赖。 - 自动
reactive:ref和reactive并不是孤立的。如果你把一个对象塞进ref,ref({ a: 1 })它会自动帮你用reactive代理这个对象。
.value:访问器:依赖收集与更新触发
RefImpl最核心的机制在于它的 .value 属性,它并不是一个普通属性,而是一个get / set访问器。
typescript
// RefImpl 类的 .value 访问器
class RefImpl<T> {
// ...
// 当读取 .value 属性时 (get)
get value() {
// 1. 【依赖收集】
// 调用 track(),将当前正在运行的 effect 注册到 dep
this.dep.track()
// 2. 返回内部存储的值
return this._value
}
// 当设置 .value 属性时 (set)
set value(newValue) {
// 1. 检查新旧值是否有变化
if (hasChanged(newValue, this._rawValue)) {
// 2. 更新内部的原始值和处理后的值
this._rawValue = newValue
this._value = toReactive(newValue) // 新值同样需要尝试转为 reactive
// 3. 【派发更新】
// 通知所有订阅了此 ref 的 effect 重新运行
this.dep.trigger()
}
}
}ref 的全部实现原理就是:
- 将一个值(原始值或对象)包装成
RefImpl实例。 get value访问器负责依赖收集 (track)。set value访问器负责派发更新 (trigger)。
shallowRef:浅层 ref
ref 会自动对传入的对象进行 reactive 深层代理。有时我们有一个大型对象,希望跳过这个深层代理,只在 .value 根引用被替换时才更新,这时就可以使用 shallowRef
答案是 shallowRef,一个“懒盒子”。
typescript
// ref 和 shallowRef 都由 createRef 工厂创建
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue // 如果传入的已经是 ref,直接返回
}
// shallow 标志位会传递给 RefImpl 的构造函数
return new RefImpl(rawValue, shallow)
}
export function ref(value?: unknown) {
return createRef(value, false) // ref 传入 shallow: false
}
export function shallowRef(value?: unknown) {
return createRef(value, true) // shallowRef 传入 shallow: true
}shallowRef 的“懒”体现在两个地方:
set value(newVal):在RefImpl构造函数和set中,shallow为true会阻止toReactive(newValue)的调用。盒子会原封不动地存入新值。- 触发时机:它只在
.value本身被替换时(state.value = newObj)才会触发。你修改state.value.count = 2是无效的,因为盒子里的对象不是响应式的,盒子本身也不知道它内部发生了变化。
何时使用? 当你有一个巨大的、不可变的数据结构,你只想在它整个被替换时才更新视图,
shallowRef是绝佳的性能优化选择。
toRefs:解决 reactive 解构丢失响应性的问题
ref 最重要的应用场景之一,是解决 reactive 最大的痛点:解构会丢失响应性。
javascript
const state = reactive({ count: 0, name: 'Vue' })
// 致命操作:解构
// count 只是一个普通的数字 0,它和 state.count 已经没关系了
// 改变 count 不会触发任何更新
const { count, name } = state为了解决这个问题,Vue 提供了 toRefs。
javascript
// 正确的做法
const stateAsRefs = toRefs(state)
// stateAsRefs 是 { count: Ref<0>, name: Ref<'Vue'> }
const { count, name } = stateAsRefstoRefs 是如何把 reactive 对象的属性,变成一个个 ref 的呢?它创建的 ref 和 RefImpl 可不一样,而是 ObjectRefImpl。
typescript
// toRef (toRefs 是 toRef 的批量调用)
function propertyToRef(source: Record<string, any>, key: string) {
return new ObjectRefImpl(source, key)
}
// ObjectRefImpl 的实现
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly [ReactiveFlags.IS_REF] = true
constructor(private readonly _object: T, private readonly _key: K) {}
// get value:
// 它的 get 访问器,会“委托”给原始 reactive 对象的属性读取
get value() {
return this._object[this._key]
}
// set value:
// 它的 set 访问器,会“委托”给原始 reactive 对象的属性设置
set value(newVal) {
this._object[this._key] = newVal
}
}ObjectRefImpl 是一个天才设计:
- 它本身不存储任何值。
- 它是一个
ref(有.value),所以你可以安全地解构和传递它。 - 它的
get/set直接访问原始reactive对象的属性。 - 由于它访问了原始
reactive对象,它会触发原始对象的代理(Proxy),从而自动完成了依赖收集和触发。
toRefs 为 reactive 对象的每个属性都创建了一个“属性引用”,确保解构后响应性不丢失。
自动解包 (proxyRefs):为什么模板里不用 .value
我们费尽心机地使用了 .value,可为什么在 <template> 里却可以直接写 ,而不是 count.value
因为 Vue 又帮我们做了一层代理,proxyRefs。
当我们的 setup() 函数返回一个 { count: ref(0) } 对象时,Vue 会立刻用 proxyRefs 把这个对象包起来。
typescript
// core/packages/reactivity/src/ref.ts
// proxyRefs 的处理器
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => {
// 1. 正常读取属性值
const value = Reflect.get(target, key, receiver)
// 2. 【自动解包】
// 如果读出的是一个 ref,则自动返回它的 .value
return isRef(value) ? value.value : value
},
set: (target, key, value, receiver) => {
const oldValue = target[key]
// 3. 【智能写入】
// 如果旧值是 ref,而新值不是 ref,
// 则自动写入旧值的 .value 属性
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
// 否则,正常替换
return Reflect.set(target, key, value, receiver)
}
},
}
// setup() 返回的对象会被这样包装
proxy = new Proxy(objectWithRefs, shallowUnwrapHandlers)proxyRefs 是一个 Proxy 代理。它在 get 时自动解包 .value,在 set 时智能写入 .value``。reactive 在设置属性时也会使用此逻辑,这就是 reactive({ a: ref(0) }) 也可以自动解包的原因。
总结
Vue 3 的 ref 系统是一套用于包装值的响应式机制:
RefImpl:是ref的核心实现,它是一个包装类,通过.value的get/set访问器来调用track和trigger。refvsshallowRef:ref会对对象值自动调用reactive;shallowRef则不会,它只对.value自身的替换操作做出响应。toRefs/ObjectRefImpl:用于解决reactive解构丢失响应性的问题。它创建的ref会将get/set操作委托回源reactive对象的Proxy。proxyRefs(自动解包):用于setup的返回值,它在get时自动返回ref.value,让我们在模板中可以省略.value。
