6.1 引入 ref 的概念
原始值(number / string / boolean 等)无法被 Proxy 代理,用 ref 把原始值「包裹」成对象,从而获得响应能力。
一、为什么原始值不能直接用 Proxy
- Proxy 的代理目标必须是非原始值,
new Proxy(1, {})会直接抛错。 - 因此我们没有任何手段拦截对原始值的读 / 写操作,既无法
track收集依赖,也无法trigger触发响应。 - 唯一可行的思路:用一个非原始值(对象)去「包裹」原始值,再对包裹对象做响应式处理。
二、ref 的最小实现
function ref(val) {
// 1. 创建包裹对象 wrapper,用 value 属性承载原始值
const wrapper = { value: val }
// 2. 打标记:标识这是一个 ref
Object.defineProperty(wrapper, '__v_isRef', { value: true })
// 3. 用 reactive 把包裹对象变成响应式数据
return reactive(wrapper)
}
使用:
const refVal = ref(1)
effect(() => {
console.log(refVal.value) // 读取必须通过 .value
})
refVal.value = 2 // 走 reactive 的 set 拦截 → 触发副作用函数重新执行
ref的本质就是 reactive + 一层 value 包裹:把「对原始值的操作」转换成「对普通对象的属性读写」。- 挂到对象属性上之后,读写就走 Proxy 的
get/set拦截,track/trigger全部生效。 - 属性名约定为
value,这也是ref必须写.value的原因。
易错点:
ref(1)返回的是对象不是数字,refVal + 1会得到" [object Object]1"这类结果,必须写refVal.value。
三、为什么要 __v_isRef 标识
- 问题:拿到一个响应式数据,怎么区分它是原始值的包裹对象(ref),还是普通非原始值的响应式数据?
- 做法:用
Object.defineProperty在 wrapper 上定义__v_isRef属性,值为true。 - 该属性不可枚举、不可写,因此不会被
for...in、Object.keys、展开运算符带到别处,也不会被意外改写。 - 之后只需检查
__v_isRef就能判断一个数据是不是 ref —— 6.2 的toRef、6.3 的proxyRefs都依赖这个标识。
相关
- 6.2 响应丢失问题:展开响应式对象会让属性丢失响应,用 toRef / toRefs 转换回 ref