在之前的文章中,我们讲解到
reactive解决了对象的响应式问题,但面对原始值(如字符串、数字、布尔值),它却无能为力。ref的出现填补了这个空白,并通过.value这个精巧的设计,让所有类型的数据都能被响应式系统管理。
前言:为什么需要 ref?
当我们在使用 reactive 定义个一个响应式对象是,这是没有问题的
const state = reactive({ count: 0 });
state.count++;
但如果我们想单独管理一个原始值呢?
let count = 0;
此时,ref 就派上了用场:
const countRef = ref(0);
countRef.value++;
更神奇的是,在模板中可以直接使用,不需要 .value :
<!-- 自动解包 -->
<div>{{ countRef }}</div>
原始值的响应式包装
为什么需要 .value?
在 Vue3 中,是通过 Proxy 代理实现的响应式系统;但 Proxy 无法对原始值进行代理,只能通过对象包装。于是 Vue3 设计时,通过创建一个包含 .value 属性的对象,对这个对象进行代理:
let count = 0; // 原始值,无法被代理
let countRef = {value: conut}; // 使用 value 包裹成对象,可以被代理
RefImpl 类的实现
为什么需要 RefImpl 类?
在上述代码中,我们确实可以通过 ref 来创建一个包含 .value 的对象,并对这个对象进行代理,但这也会带来新的问题:
const refVal = ref(1);
const reactiveRef = reactive({value : 1});
上述代码中,refVal 和 reactiveRef 在实现和使用上没有任何区别,那我们如何区别一个数据到底是不是 ref 呢?
针对这些新引发的问题, Vue3 开发团队实现了 RefImpl 类来解决这些问题。
RefImpl 类的好处
- 类型标识:使用
class创建的实例,其原型链上明确指向RefImpl;同时在内部维护__v_isRef属性标识,使其更加规范。 - 性能优化: 所有
RefImpl实例共享同一个原型(RefImpl.prototype),把方法挂在原型上,相比于在每个工厂函数返回的对象上都复制一遍方法,内存利用率更高。 - 可维护性:
RefImpl除了处理基本类型,还需要处理对象类型的特殊情况。如果给ref传入一个对象,Vue3需要将其转换为reactive,这部分逻辑放在构造函数中处理会非常清晰。 - 响应式标记:
RefImpl在原型上定义了__v_isRef属性,这使得内部工具函数(如isRef、unref)能够准确识别ref对象。如果使用普通函数,需要手动为每个返回的对象定义这个属性,增加了代码冗余。
RefImpl 类的内部实现
class RefImpl {
_rawValue; // 原始值(未处理)
_value; // 响应式值(可能被 reactive 包装)
__v_isRef;
constructor(value) {
// 保存原始值
this._rawValue = value;
// ref 直接存值
this._value = value;
this.__v_isRef = true; // 标记为 ref
}
// 访问 .value 时收集依赖
get value() {
track(this, 'value'); // Ref 的依赖收集目标是自身,key 固定为 'value'
return this._value;
}
// 修改 .value 时触发更新
set value(newValue) {
// 新旧值不同才更新(避免无效触发)
if (newValue !== this._rawValue) {
this._rawValue = newValue;
// 浅 ref 直接赋值
this._value = newValue;
trigger(this, 'value'); // 触发 Ref 的 'value' 属性更新
}
}
}
ref 函数的基础实现
function ref(value) {
return new RefImpl(value, false);
}
ref 的自动解包
在模板中的自动解包
Vue3 模板编译器会自动对 ref 进行 .value 解包:
- 在模板中直接使用
{{ ref }} - 在指令中直接使用
v-bind="ref" - 在事件处理中直接使用
ref
function templateCompiler(template, context) {
// 简化版模板编译
const refPattern = /\{\{\s*(\w+)\s*\}\}/g;
let match;
while ((match = refPattern.exec(template)) !== null) {
const varName = match[1];
if (context[varName] && context[varName].__v_isRef) {
console.log(`${varName} 是 ref,编译为 ${varName}.value`);
} else {
console.log(`${varName} 是普通变量,保持不变`);
}
}
// 模拟编译结果
const compiled = template.replace(refPattern, (match, varName) => {
return context[varName] && context[varName].__v_isRef
? '${' + varName + '.value}'
: '${' + varName + '}';
});
return compiled;
}
模版编译在后面的章节会专门介绍。
在 reactive 中的自动解包
function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
const value = Reflect.get(target, key)
// 如果是 ref,自动解包返回 .value;否则返回原值
return isRef(value) ? value.value : value
},
set(target, key, newValue) {
const value = Reflect.get(target, key)
// 如果原有值是 ref,修改其 .value;否则直接赋值
if (isRef(value)) {
value.value = newValue
return true
}
return Reflect.set(target, key, newValue)
},
})
}
ref 工具函数集
isRef 类型判断
function isRef(r) {
return !!(r && r.__v_isRef === true);
}
shallowRef - 浅层 ref
shallowRef 会创建一个只追踪 .value 变化的 ref,对于 .value 中的对象,不进行深层响应式,其适用场景如下:
- 大型对象,不需要深层响应式
- 与第三方库集成
- 性能优化
class RefImpl {
_rawValue; // 原始值(未处理)
_value; // 响应式值(可能被 reactive 包装)
__v_isRef;
constructor(value, isShallow) {
// 保存原始值
this._rawValue = value;
// 浅 ref 直接存值;深 ref 对对象类型用 reactive 包装
this._value = isShallow ? value : toReactive(value);
this.__v_isRef = true; // 标记为 ref
}
// 访问 .value 时收集依赖
get value() {
track(this, 'value'); // Ref 的依赖收集目标是自身,key 固定为 'value'
return this._value;
}
// 修改 .value 时触发更新
set value(newValue) {
// 新旧值不同才更新(避免无效触发)
if (newValue !== this._rawValue) {
this._rawValue = newValue;
// 浅 ref 直接赋值;深 ref 对对象类型用 reactive 包装
this._value = this.isShallow ? newValue : toReactive(newValue);
trigger(this, 'value'); // 触发 Ref 的 'value' 属性更新
}
}
}
function shallowRef(value) {
return new RefImpl(value, true);
}
toRef - 为响应式对象的属性创建 ref
toRef 会为响应式对象的属性创建一个 ref,这个 ref 保持与原属性的响应式连接,其适用场景如下:
- 将对象的某个属性作为 ref 传递给函数
- 解构时保持响应式
- 创建对对象属性的引用
class ObjectRefImpl {
__v_isRef = true;
constructor(_object, _key) {}
// 访问 .value 时指向原对象的属性
get value() {
return this._object[this._key]
}
// 修改 .value 时同步到原对象的属性
set value(newValue) {
this._object[this._key] = newValue
}
}
// 将对象的单个属性转为 ref(保持引用关系)
function toRef(object, key) {
return new ObjectRefImpl(object, key)
}
toRefs - 批量转换
toRefs 会将响应式对象的所有属性转换为 ref,保持响应式连接,其适用场景如下:
- 从 reactive 对象解构属性
- 在组合式函数中返回多个值
- 将对象属性作为独立的 ref 使用
// 将对象的所有属性转为 ref(批量 toRef)
function toRefs(object) {
const result = {};
// 遍历对象的自有属性
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
result[key] = toRef(object, key);
}
}
return result;
}
unref - 解包 ref
// 解包 ref:如果是 ref 返回 .value,否则返回自身
function unref(ref) {
return isRef(ref) ? ref.value : ref
}
完整 ref 的实现
class RefImpl {
_rawValue; // 原始值(未处理)
_value; // 响应式值(可能被 reactive 包装)
__v_isRef;
constructor(value, isShallow) {
// 保存原始值
this._rawValue = value;
// 浅 ref 直接存值;深 ref 对对象类型用 reactive 包装
this._value = isShallow ? value : toReactive(value);
this.__v_isRef = true; // 标记为 ref
}
// 访问 .value 时收集依赖
get value() {
track(this, 'value'); // Ref 的依赖收集目标是自身,key 固定为 'value'
return this._value;
}
// 修改 .value 时触发更新
set value(newValue) {
// 新旧值不同才更新(避免无效触发)
if (newValue !== this._rawValue) {
this._rawValue = newValue;
// 浅 ref 直接赋值;深 ref 对对象类型用 reactive 包装
this._value = this.isShallow ? newValue : toReactive(newValue);
trigger(this, 'value'); // 触发 Ref 的 'value' 属性更新
}
}
}
// 创建 ref 对象
function ref(value) {
if (isRef(value)) {
return value;
}
return new RefImpl(value, false);
}
// isRef 类型判断
function isRef(r) {
return !!(r && r.__v_isRef === true);
}
// 代理 ref 对象:访问时自动解包 .value,修改时自动包裹
function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
const value = Reflect.get(target, key)
// 如果是 ref,自动解包返回 .value;否则返回原值
return isRef(value) ? value.value : value
},
set(target, key, newValue) {
const value = Reflect.get(target, key)
// 如果原有值是 ref,修改其 .value;否则直接赋值
if (isRef(value)) {
value.value = newValue
return true
}
return Reflect.set(target, key, newValue)
},
})
}
// 浅 ref:仅 .value 本身响应式,对象类型不递归处理
function shallowRef(value) {
return new RefImpl(value, true);
}
class ObjectRefImpl {
__v_isRef = true;
constructor(_object, _key) {}
// 访问 .value 时指向原对象的属性
get value() {
return this._object[this._key];
}
// 修改 .value 时同步到原对象的属性
set value(newValue) {
this._object[this._key] = newValue;
}
}
// 将对象的单个属性转为 ref(保持引用关系)
function toRef(object, key) {
return new ObjectRefImpl(object, key);
}
// 将对象的所有属性转为 ref(批量 toRef)
function toRefs(object) {
const result = {};
// 遍历对象的自有属性
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
result[key] = toRef(object, key);
}
}
return result;
}
// 解包 ref:如果是 ref 返回 .value,否则返回自身
function unref(ref) {
return isRef(ref) ? ref.value : ref
}
ref 与 reactive 的选择场景
使用 ref 的场景
- 基本类型数据(string, number, boolean)
- 需要解构的属性
- 在组合式函数中返回的值
- 需要 .value 显式访问(明确性是优势)
- 传递给需要 ref 参数的函数
使用 reactive 的场景
- 对象和数组
- 深层的响应式数据结构
- 不需要频繁解构
- 习惯使用普通对象语法
- 性能敏感的大对象(少一层代理)
混用建议
- reactive 中可以包含 ref(会自动解包)
- ref 中也可以包含对象(但不推荐)
- 在组件的 setup 中,推荐使用 ref(一致性)
- Vue 3.2+ 推荐默认使用 ref
结语
ref 系统与 reactive 共同构成了 Vue3 完整的响应式体系,理解它们的实现原理,能够帮助我们在开发中做出更明智的技术选择。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!