实现 Ref
约 2047 字大约 7 分钟
2026-03-29
为什么需要
ref到这里,
reactive已经能把对象变成响应式数据了,但它还有两个明显的边界没有覆盖到:Proxy只能代理对象,不能直接代理原始值- 响应式对象一旦被解构,读写就很容易脱离原来的代理对象,进而丢失响应性
所以
ref本质上就是在补这两个能力:- 让原始值也能具备响应式能力
- 给对象属性提供一个稳定的“引用壳”,避免解构后直接断开和原对象的联系
先实现最简单的
ref对原始值来说,既然不能直接交给
Proxy,那最直接的办法就是先包一层对象,然后继续复用前面已经实现好的reactiveconst count = ref(1); effect(() => { layer1.innerHTML = count.value + ""; }); btn1.addEventListener("click", () => { count.value++; });关键点
ref的第一步其实并不复杂,核心就是把原始值包装成一个带有value属性的对象,再把这个对象交给reactive。这样外部读取的是count.value,内部真正被依赖收集和触发更新的也是这个value代码
ref.tsfunction ref(value) { const wrapper = { value, }; return reactive(wrapper); }index.tsconst count = ref(1); effect(() => { console.log(count.value); });给
ref增加标记上面的实现虽然已经能跑起来了,但马上就会遇到一个问题:如果某个普通响应式对象本身也有一个
value属性,那它和ref从表面上就很难区分const ref1 = ref(1); const ref2 = reactive({ value: 1 });所以这里还需要再补一个固定标记,用来明确区分“这是一个
ref”还是“这只是一个普通对象”关键点
这里并不是靠
value属性来识别ref,而是额外定义一个固定的内部标记。后面无论是isRef、toRef还是proxyRefs,本质上都是通过这个标记来判断当前值是不是ref代码
ref.tsfunction ref(value) { const wrapper = { value, }; Object.defineProperty(wrapper, "__v_isRef", { value: true, writeable: false, }); return reactive(wrapper); }用
toRef处理单个属性的响应式丢失ref的第二个作用,是处理响应式对象在解构、展开、单独取值之后的响应式丢失问题const stateObj = reactive({ name: "jack", age: 18, }); const deObj = { ...stateObj };这里的
deObj已经只是一个普通对象了,后面再去修改stateObj.name,依赖deObj.name的地方不会更新所以这里要做的,不是把属性值拷贝出来,而是给这个属性包一层访问器,让后续读取重新回到原对象上
关键点
toRef返回的不是原值,而是一个带有value访问器的包装对象。读取wrapper.value时,本质上会转成对object[key]的再次读取;这样依赖收集和触发更新仍然发生在原来的响应式对象上代码
ref.tsfunction toRef(obj, key) { const wrapper = { get value() { return obj[key]; }, set value(newVal) { obj[key] = newVal; }, }; Object.defineProperty(wrapper, "__v_isRef", { value: true, writeable: false, }); return wrapper; }index.tsconst stateObj = reactive({ name: "jack", age: 18, }); const nameRef = toRef(stateObj, "name"); effect(() => { layer1.innerHTML = nameRef.value; });用
toRefs批量处理对象属性单独写
toRef(stateObj, "name")当然能解决问题,但一旦对象属性变多,这种一个一个转换的方式就会变得很笨重const newObj = { name: toRef(stateObj, "name"), age: toRef(stateObj, "age"), };所以这里就可以继续往前走一步:直接遍历对象,把每个属性都包装成
ref关键点
toRefs本身并没有引入新的响应式逻辑,它只是批量调用toRef。因此真正负责把对象属性和value重新连起来的,还是toRef返回的那层访问器包装代码
ref.tsfunction toRefs(obj) { const ret = {}; for (const key in obj) { ret[key] = toRef(obj, key); } return ret; }index.tsconst stateObj = reactive({ name: "jack", age: 18, }); const newObj = { ...toRefs(stateObj) };用
proxyRefs处理自动脱ref到这里,
ref和toRefs已经能正常工作了,但访问方式还是会变成大量的xxx.valueconst newObj = { ...toRefs(stateObj) }; console.log(newObj.name.value); console.log(newObj.age.value);这也是为什么 Vue 还要再补一层
proxyRefs。它的作用不是重新实现一遍响应式,而是再包一层Proxy,让读取属性时自动把ref.value解出来;对应地,设置属性时如果旧值本身是ref,就把赋值转发到旧ref的value上关键点
proxyRefs处理的是“访问体验”而不是“依赖逻辑”。get时判断结果是不是ref,如果是就返回value;set时判断旧值是不是ref,如果是就改它的value,否则再走普通的Reflect.set代码
ref.tsfunction proxyRefs(target) { return new Proxy(target, { get(target, key, receiver) { const result = Reflect.get(target, key, receiver); return result.__v_isRef ? result.value : result; }, set(target, key, value, receiver) { const oldValue = target[key]; if (oldValue && oldValue.__v_isRef) { oldValue.value = value; return true; } return Reflect.set(target, key, value, receiver); }, }); }index.tsconst stateObj = reactive({ name: "jack", age: 18, }); const deNewObj = proxyRefs({ ...toRefs(stateObj) }); console.log(deNewObj.name); console.log(deNewObj.age);用
RefImpl重新整理ref的实现前面的逻辑在 JavaScript 里已经讲清楚了,接下来就可以把它正式收口到 TypeScript 版本中
这里主要做了三件事:
- 把
Ref接口单独挪到ref.ts - 用
isRef判断当前值是不是已经是ref - 用
createRef + RefImpl把ref和shallowRef统一收口
关键点
createRef这一步做的其实是边界收口:如果传进来的本身已经是ref,那就直接返回;否则统一走RefImpl。而RefImpl里真正负责响应式行为的,还是value的访问器属性以及_rawValue / _value这两个状态代码
ref.tsexport interface Ref<T = any> { value: T; } export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>; export function isRef(r: any): r is Ref { return Boolean(r && r.__v_isRef === true); } export function ref(value?: any): any { return createRef(value); } export function shallowRef(value?: any): any { return createRef(value, true); } function createRef(rawValue: unknown, shallow = false) { if (isRef(rawValue)) { return rawValue; } return new RefImpl(rawValue, shallow); } const convert = <T extends unknown>(val: T): T => { return isObject(val) ? reactive(val) : val; }; class RefImpl<T> { private _value: T; public readonly __v_isRef = true; constructor( private _rawValue: T, private readonly _shallow: boolean, ) { this._value = _shallow ? _rawValue : convert(_rawValue); } get value() { track(toRaw(this), TrackOpTypes.GET, "value"); return this._value; } set value(newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal; this._value = this._shallow ? newVal : convert(newVal); trigger(toRaw(this), TriggerOpTypes.SET, "value", newVal); } } }- 把
把
toRef和toRefs的类型补上到了 TypeScript 这一层,
toRef和toRefs处理的不再只是运行时逻辑,还要让类型系统知道:当前返回的是“包住对象属性的ref”关键点
toRef<T, K>的核心是让第二个参数key受第一个参数object约束,并且把返回值精确收成Ref<T[K]>。toRefs<T>则是在这个基础上再做一层映射,把对象上每个属性都转换成对应的Ref代码
ref.tsexport function toRef<T extends object, K extends keyof T>( object: T, key: K, ): Ref<T[K]> { return isRef(object[key]) ? object[key] : (new ObjectRefImpl(object, key) as any); } class ObjectRefImpl<T extends object, K extends keyof T> { public readonly __v_isRef = true; constructor( private _object: T, private _key: K, ) {} get value() { return this._object[this._key]; } set value(newVal) { this._object[this._key] = newVal; } } export type ToRefs<T = any> = { [K in keyof T]: Ref<T[K]> }; export function toRefs<T extends object>(object: T): ToRefs<T> { const ret: any = isArray(object) ? new Array(object.length) : {}; for (const key in object) { ret[key] = toRef(object, key); } return ret; }把
proxyRefs和unref收口成最终版本最后一步,就是把自动脱
ref这层逻辑也整理成正式的 TypeScript 版本这里多出来的
unref很关键,因为它把“如果是ref就取value,否则原样返回”这层判断单独抽了出去,后面proxyRefs的get就能直接复用关键点
unref负责收口“读取时脱 ref”的逻辑,proxyRefs负责把这套规则挂到对象访问上。类型上则通过ShallowUnwrapRef<T>把Ref<V>展开成V,这样读取proxyRefs(...)返回值时,外部拿到的就不再是Ref,而是已经脱过一层的值类型代码
ref.tsexport type ShallowUnwrapRef<T> = { [K in keyof T]: T[K] extends Ref<infer V> ? V : T[K]; }; export function unref<T>(ref: T): T extends Ref<infer V> ? V : T { return isRef(ref) ? (ref.value as any) : ref; } export const shallowUnwrapHandlers: ProxyHandler<any> = { get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)), set: (target, key, value, receiver) => { const oldValue = target[key]; if (isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } return Reflect.set(target, key, value, receiver); }, }; export function proxyRefs<T extends object>( objectWithRefs: T, ): ShallowUnwrapRef<T> { return new Proxy(objectWithRefs, shallowUnwrapHandlers); }index.tsconst state = reactive({ name: "jack", age: 18, }); const proxyState = proxyRefs({ ...toRefs(state) }); console.log(proxyState.name); proxyState.name = "tom";
