1505 字
8 分钟
Vue 3 的 toRef、toRefs 与 toRaw

toRef 让属性变成 ref#

toRef(state, 'name') 返回一个 ref,它的 .value 直接读写 state.name。两边是同一份数据,改任何一边另一边都跟着变。

const state = reactive({ name: '小明' })
const nameRef = toRef(state, 'name')
nameRef.value = '小红'
console.log(state.name) // 小红
state.name = '小刚'
console.log(nameRef.value) // 小刚

它和 ref(state.name) 不一样。ref 收到的是一个普通字符串,包起来之后就和 state.name 断了联系;toRef 不存值,只是记下对象和属性名,每次都现取。

源对象是不是响应式决定视图更不更新#

toRef 只负责建立读写通道,视图更新与否取决于这条通道底下有没有依赖收集。

源是 reactive 对象时,读 .value 会走代理的 get,触发 track;写 .value 会走 set,触发 trigger,视图跟着更新。

源是普通对象时,读写就是普通的属性访问,没有代理介入,track 和 trigger 都不会发生。数据能改,但视图不会重新渲染。

属性不存在时也能用#

toRef(object, key) 这种写法下,即使源对象当前没有这个属性,也会返回一个可用的 ref。这样就能先处理可选属性,等它后面再出现。

toRefs 做不到这一点:它只遍历调用那一刻已存在的可枚举属性。

props 上的 ref 不能赋值#

把 props 的属性包成 ref 再传给组合式函数是很常见的用法:

useSomeFeature(toRef(props, 'foo'))

但 props 是只读的,给这个 ref 赋值等于直接改 prop,Vue 不允许。控制台会给出警告,需要可写的场景要改用带 set 的 computed。

toRefs 批量转换#

toRefs 把对象的每个属性都转成 ref,返回一个普通对象(传数组进去时返回的是数组)。写法上就是遍历一遍,逐个调用内部的属性转换函数。

packages/reactivity/src/ref.ts
// @__NO_SIDE_EFFECTS__
function toRefs(object) {
if (__DEV__ && !isProxy(object)) {
warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret = shared.isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = propertyToRef(object, key)
}
return ret
}

上面这段来自 Vue 3.5.43 的 packages/reactivity/src/ref.ts。它有三处容易被忽略的细节。

参数不是代理时,isProxy(object) 为假,开发环境会打印一条警告,提示 toRefs() 期望收到响应式对象。

返回值用 isArray(object) ? new Array(object.length) : {} 分配,所以传数组进去拿到的还是数组,不是普通对象。

循环体调用的是 propertyToRef,不是 toRef。3.5.43 里 propertyToRef 只做一件事:

packages/reactivity/src/ref.ts
function propertyToRef(source, key, defaultValue) {
return new ObjectRefImpl(source, key, defaultValue)
}

枚举不到的属性拿不到 ref#

toRefs 的转换走 for...in,只遍历调用那一刻可枚举的属性。

所以后来才加上去的属性不会有对应的 ref,一开始就不存在的可选属性也不会有。这类属性要用 toRef 单独处理。

另一个后果是 toRefs 只做一层,嵌套对象内部转成的是指向那个对象的 ref,不是把内层属性也铺平。

ObjectRefImpl 的实现#

toRef 返回的对象由 ObjectRefImpl 构造,它不存值,只记下源对象和属性名。下面这个类来自 Vue 3.5.43 的 packages/reactivity/src/ref.ts。

packages/reactivity/src/ref.ts
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
public _value: T[K] = undefined!
private _key: K
private _raw: T
private _shallow: boolean
constructor(
private readonly _object: T,
key: K,
private readonly _defaultValue?: T[K],
) {
this._key = key
this._raw = toRaw(_object)
let shallow = true
let obj: any = _object
// 数组的整数下标不解包,其余情况逐层检查代理是否需要解包
if (
!shared.isArray(_object) ||
shared.isSymbol(key) ||
!shared.isIntegerKey(key)
) {
do {
shallow = !isProxy(obj) || isShallow(obj)
} while (shallow && (obj = obj['__v_raw']))
}
this._shallow = shallow
}
get value(): T[K] {
let val = this._object[this._key]
if (this._shallow) {
val = unref(val)
}
return (this._value = val === undefined ? this._defaultValue : val)
}
set value(newVal: T[K]) {
// 浅层时属性本身可能是 ref,赋值要转发进去
if (this._shallow && isRef(this._raw[this._key])) {
const nestedRef = this._object[this._key]
if (isRef(nestedRef)) {
nestedRef.value = newVal
return
}
}
this._object[this._key] = newVal
}
get dep(): Dep | undefined {
return getDepFromReactive(this._raw, this._key)
}
}

这个类里有几个字段,简化版容易漏掉:

  • _raw:保存 toRaw(_object) 的结果,供 set value 和 dep 使用
  • _key:构造时传入的属性名
  • _shallow:这个 ref 是不是浅层的
  • _defaultValue:构造时传入的默认值,get value 取到 undefined 时回退到它

get value 在浅层情况下会先 unref(val),所以属性本身是 ref 时会被解包。

set value 有一处分支:属性本身已经是 ref 且当前是浅层时,赋值会转发到那个内层 ref 的 .value,而不是替换属性。这个分支是 3.5.x 之后才加的。

dep 是一个 getter,通过 getDepFromReactive(this._raw, this._key) 到依赖表里找到该属性对应的 Dep。有了它,watch 这类需要拿到依赖对象的内部逻辑才有办法在 ref 上工作。

toRaw 取出原始对象#

toRaw 返回代理背后的原始对象。它读代理上的 __v_raw 标记,拿到之后递归继续取,直到某个值不再带标记为止。

packages/reactivity/src/reactive.ts
// @__NO_SIDE_EFFECTS__
function toRaw(observed) {
const raw = observed && observed['__v_raw']
return raw ? toRaw(raw) : observed
}

递归这一层有实际意义。readonly 包在 reactive 外面时,代理会套两层,只有一路取到底才能拿到真正的原始对象。

拿到的原始对象不带代理,改它不会触发 trigger,视图不会更新。适合用在不想被追踪、需要交给第三方库、或者调试时对比数据的场景。

三个 API 对照#

API返回什么和源的关系
toRef单个属性的 ref双向同步,不存值
toRefs每个属性一个 ref 的普通对象双向同步,只覆盖调用时可枚举的属性
toRaw原始对象本身同一个引用,读写不触发响应式

第 6 篇,共 19 篇本文属于系列

Vue 3 的 toRef、toRefs 与 toRaw
https://www.choria.top/posts/vue3-toref-reactive/
作者
乔瑞雅·克林威尔
发布于
2026-04-08
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 214 天

部分内容可能已过时