在响应式系统中,
reactive
能够将一个对象转换为深层的响应式对象,但是在开发过程中,我们时常会需要用到解构赋值,这时候会导致响应性丢失。
问题解析
HTML
<body>
<div id="app"></div>
<script type="module">
import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { reactive, effect, ref } from '../dist/reactivity.esm.js'
const state = reactive({
name: 'a',
age: 18
})
const { name } = state // 解构赋值
effect(() => {
console.log(name) // 打印的是 'a',一个普通的字符串
})
setTimeout(() => {
state.name = 'b' // 这里的修改无法被 effect 侦测到
}, 1000)
</script>
</body>
执行这段代码,你会发现解构出来的属性会丢失响应式,所以 setTimeout
不会触发更新。

为了解决上述问题,我们通常会用 toRef
,让解构出来的变量可以触发响应式更新:
HTML
<body>
<div id="app"></div>
<script type="module">
import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { reactive, effect, ref } from '../dist/reactivity.esm.js'
const state = reactive({
name: 'a',
age: 18
})
const name = toRef(state, 'name') // 使用 toRef
effect(() => {
console.log(name.value) // 需要通过 .value 访问
})
setTimeout(() => {
state.name = 'b'
}, 1000)
</script>
</body>

核心原理
如果这时候去看这个 name
输出的类型:

你会发现它跟我们在使用的 RefImpl
类型不同,它是一个特制的 ObjectRefImpl
类,并且多了两个属性 _object
、_key
,它们分别存储了原始对象、属性名称。
这个 toRef
我们可以知道它接受一个对象以及 key,所以我们可以这样写:
TypeScript
// ref.ts
export function toRef(target, key) {
return {
get value() {
return target[key]
},
set value(newValue) {
target[key] = newValue
}
}
}
这样其实就可以更新,但官方示例是属于一个类,所以我们也改写成类:
TypeScript
class ObjectRefImpl {
[ReactiveFlags.IS_REF] = true // 标记为 ref
constructor(public _object, public key) {}
get value() {
// 访问 .value 时,代理到原始对象的对应 key
return this._object[this.key]
}
set value(newValue) {
// 设置 .value 时,代理到原始对象的对应 key
this._object[this.key] = newValue
}
}
export function toRef(target, key) {
return new ObjectRefImpl(target, key)
}
这样就可以将我们解构出来的变量,重新赋予响应性。
toRefs
当需要处理多个属性时,可以使用 toRefs
,它会遍历一个 reactive
对象,并将其所有属性都转换为 ref
,使用如下:
HTML
<body>
<div id="app"></div>
<script type="module">
import { reactive, toRefs, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { reactive, effect, toRef } from '../dist/reactivity.esm.js'
const state = reactive({
name: 'a',
age: 18
})
const { name, age } = toRefs(state) // 使用 toRefs
effect(() => {
console.log(age.value)
})
setTimeout(() => {
state.age++
}, 1000)
</script>
</body>

输出 age
之后,可以看到它也是 ObjectRefImpl
类。

那我们可以知道 toRefs
的实现非常直观,它遍历目标对象的所有 key
,并为每一个 key
调用 toRef
:
TypeScript
export function toRefs(target) {
const res = {}
for (const key in target) {
res[key] = new ObjectRefImpl(target, key)
}
return res
}
PS:toRefs
源码中还有其他判断逻辑,例如确认传入的是不是响应式对象,我们这边就先省略判断,让它可以触发更新:
虽然
toRefs
解决了响应性丢失的问题,但到处都是 .value
,所以我们这边需要两个辅助工具。
unref
unref
是一个简单的辅助函数,如果参数是 ref
,它返回 .value
;如果不是,则直接返回参数本身。
TypeScript
export function unref(value) {
return isRef(value) ? value.value : value
}
ProxyRef
proxyRefs
可以将一个包含 ref
的对象(例如 toRefs
的返回值)转换为一个特殊的代理。当访问这个代理的属性时,它会自动解包 .value
。它跟 reactive
很像,不直接用 reactive
是因为 reactive
是深层响应式的,而 proxyRefs
通常是浅层的。
TypeScript
export function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
return unref(res) // 访问时自动 unref
},
set(target, key, newValue, receiver) {
// 这里的 set 也需要处理,如果目标是 ref 而新值不是,应该设置 .value
return Reflect.set(target, key, newValue, receiver)
}
})
}
这样就完成了 proxyRefs
。
今天我们的重点在于:
- 直接从
reactive
对象中解构,会失去响应性。 - 使用
toRef
可以为单个属性创建响应式链接。 - 使用
toRefs
可以将整个对象的所有属性批量转换为ref
,再进行解构。这样每个被解构出来的变量都与原始对象保持了响应式链接。 - 选择性地使用
unref
和proxyRefs
来简化对.value
的访问。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。