6.2 响应丢失问题
用展开运算符复制响应式对象会得到一个普通对象,其属性不再具有响应能力;toRef / toRefs 把属性转换成 ref 来修复。
一、什么是响应丢失
const obj = reactive({ foo: 1, bar: 2 })
// 展开运算符只复制属性值,newObj 是普通对象
const newObj = { ...obj }
effect(() => {
console.log(newObj.foo) // 只执行一次,之后不再执行
})
obj.foo = 100 // 不会触发副作用函数重新执行 —— 响应丢失
原因:newObj 不是代理对象,读取 newObj.foo 时不经过 obj 的 get 拦截,没有建立依赖;修改 obj.foo 时自然也找不到任何副作用函数可触发。
典型场景:把响应式对象的属性展开后再传给别的函数、作为组件 props、或从
setup返回时,响应链会「断掉」。
二、解决思路:把属性转换成 ref 结构
既然 ref 自带「通过 .value 访问」的约定,那就为每个属性生成一个类似 ref 的包裹对象:
function toRef(obj, key) {
const wrapper = {
get value() { // 读:转发到 obj[key],走 obj 的 get 拦截 → 建立依赖
return obj[key]
},
set value(val) { // 写:转发给 obj[key],走 obj 的 set 拦截 → 触发响应
obj[key] = val
}
}
Object.defineProperty(wrapper, '__v_isRef', { value: true })
return wrapper
}
- 这里用的是带 getter / setter 的对象:wrapper 本身不必被 reactive 包裹,读写全部转发到已经是响应式数据的
obj上,依赖收集与触发响应都由obj负责。 - 也就是说 wrapper 只是一层「转发层」,不产生新的响应式数据。
- 用
Object.defineProperty打上__v_isRef标识后,toRef的返回值就是真正意义上的 ref,概念上可以和其他 ref 统一处理。
三、toRefs:批量转换
function toRefs(obj) {
const ret = {}
for (const key in obj) {
ret[key] = toRef(obj, key)
}
return ret
}
使用:
const obj = reactive({ foo: 1, bar: 2 })
const newObj = { ...toRefs(obj) } // 展开后每个属性都是 ref,响应不再丢失
effect(() => {
console.log(newObj.foo.value)
})
obj.foo = 100 // 触发响应
newObj.bar.value = 300 // 通过 setter 写回 obj.bar,同样触发响应
| API | 作用 | 返回值 |
|---|---|---|
toRef(obj, key) | 把单个属性转成 ref | 带 getter / setter 的 wrapper 对象 |
toRefs(obj) | 把对象第一层所有属性转成 ref | 属性值均为 ref 的新对象 |
易错点:
toRefs只处理第一层属性;深层属性拿到的是普通值。 易错点:转换后取值必须写.value,写法变了(可用proxyRefs自动脱 ref,见 6.3 自动脱ref)。
相关
- 6.1 引入 ref 的概念:ref 的包裹结构与
__v_isRef标识 - 6.3 自动脱ref:用 proxyRefs 省掉
.value的书写