前言
在 Vue3 的响应式系统中,ref 是一个核心 API,它让我们能够创建响应式的数据引用。虽然日常开发中我们频繁使用它,但你是否曾好奇过它的内部实现原理?今天,就让我们一起来揭开 ref 的神秘面纱,手动实现一个属于自己的 ref 函数!
系列文章
为了帮助你更好地理解 Vue3 响应式系统的完整实现,推荐阅读本系列的其他文章:
💡 提示
本文中涉及的部分函数可能在其他文章中实现,你可以通过上述目录查找完整实现。如果想深入研究,也可以直接查看 Vue3 源码。
什么是ref?
在深入实现之前,让我们先回顾一下 ref 的基本用法:
js
import { ref } from 'vue'
const name = ref('lucy');
console.log(name.value); // 'lucy'
name.value = 'james';
console.log(name.value); // 'james'
ref 接收一个内部值,返回一个响应式的、可更改的 ref 对象,该对象只有一个指向其内部值的属性 .value。
手动实现ref
核心架构
当我们打印 ref 函数创建的对象时,会发现它是一个 RefImpl 类型的实例。这表明在 ref 的底层实现中,有一个专门的类负责实现响应式功能。
js
/**
* 创建响应式引用
* @param {any} value - 值
* @param {boolean} shallow - 是否为浅层引用
* @returns {RefImpl} ref 实例
*/
export function ref(value, shallow) {
return createRef(value, shallow);
};
/**
* 创建 ref 的内部函数
* @param {any} rawValue - 原始值
* @param {boolean} shallow - 是否为浅层引用
* @returns {RefImpl} ref 实例
*/
function createRef(rawValue, shallow) {
// 如果传入的值已经是 ref,则直接返回该 ref,避免重复包装
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
/**
* 检查值是否为 ref
* @param {any} r - 待检查值
* @returns {boolean} 是否为 ref
*/
export function isRef(r) {
return r ? r[ReactiveFlags.IS_REF] === true : false;
}
RefImpl类实现
RefImpl 类的实现与我们在手写 Computed 实现中看到的模式有些相似:
js
/**
* Ref 实现类
* 用于创建基本的响应式引用
*/
class RefImpl {
_value; // 存储响应式值
__rawValue; // 存储原始值(用于比较)
dep = new Set(); // 依赖收集集合
[ReactiveFlags.IS_REF] = true; // 标识为 ref 对象
[ReactiveFlags.IS_SHALLOW] = false; // 是否为浅层 ref
/**
* 构造函数
* @param {any} value - 初始值
*/
constructor(value) {
this.__rawValue = value;
this._value = toReactive(value); // 如果是对象则转换为响应式
}
/**
* 获取 ref 的值
* @returns {any} 当前值
*/
get value() {
// 如果存在激活的副作用,则收集依赖
if (activeEffect) {
trackEffects(this.dep);
}
return this._value;
}
/**
* 设置 ref 的值
* @param {any} newValue - 新值
*/
set value(newValue) {
// 只有当新值不等于旧值时才更新
if (newValue !== this.__rawValue) {
this._value = toReactive(newValue);
this.__rawValue = newValue;
triggerEffects(this.dep); // 触发依赖更新
}
}
}
在上述代码中,toReactive 函数负责判断值类型:如果是对象,就通过 reactive 函数转换为响应式对象。
js
// reactive.js
/**
* 将值转换为响应式对象(如果是对象的话)
* @param {any} value - 需要转换的值
* @returns {Proxy|any} 响应式对象或原始值
*/
export function toReactive(value) {
// 如果是对象则创建响应式代理,否则返回原始值
return isObject(value) ? reactive(value) : value;
}
手动实现toRef函数
问题背景
toRef 是开发中常用的一个重要函数,它的主要作用是:为响应式对象上的单个属性创建一个"响应式引用"(Ref) 。
js
import { reactive } from 'vue';
const state = reactive({ name: '张三', age: 30 });
// 尝试解构
const { name, age } = state;
// 此时,name 和 age 只是普通的字符串和数字,不再是响应式的
// 修改它们不会触发 UI 更新
name = '李四';
// 这行代码在严格模式下会报错,因为解构出来的是常量
// 即使不报错,UI 也不会更新
toRef 函数正是为了解决这个问题而设计的。
基本用法
js
const state = reactive({
fistName: 'lee',
lastName: 'mary'
});
// 第一种用法:对象属性转换
const fistName = toRef(state, 'fistName');
const lastName = toRef(state, 'lastName');
// 第二种用法:计算属性转换
const fullName = toRef(() => `${firstName.value} ${lastName.value}`);
setTimeout(() => (state.firstName = "tom"), 1000);
// 一秒钟后:
// toRef 的 firstName 和 lastName 会同步更新
// fullName 也会自动更新
具体实现
js
/**
* 将值、对象属性或 getter 函数转换为 ref
* @param {any} source - 源值、对象或函数
* @param {string} key - 属性键(可选)
* @param {any} defaultValue - 默认值(可选)
* @returns {RefImpl|GetterRefImpl|ObjectRefImpl} 对应的 ref 实例
*/
export function toRef(source, key, defaultValue) {
// 如果源已经是 ref,直接返回
if (isRef(source)) {
return source;
}
// 如果源是函数,创建 getter ref
else if (isFunction(source)) {
return new GetterRefImpl(source);
}
// 如果源是对象且提供了键,创建对象属性 ref
else if (isObject(source) && arguments.length > 1) {
return propertyToRef(source, key, defaultValue);
}
// 否则创建普通 ref
else {
return ref(source);
}
}
上述代码根据数据源 source 的不同类型进行相应处理:
- 如果已经是
Ref,直接返回 - 如果是函数,交给
GetterRefImpl类处理 - 如果是对象且有第二个参数,交给
propertyToRef函数处理 - 其他情况,生成普通的
RefImpl实例
GetterRefImpl 类的实现
js
/**
* getter 引用实现类
* 用于将 getter 函数转换为响应式 ref
*/
class GetterRefImpl {
[ReactiveFlags.IS_REF] = true; // 标识为 ref 对象
_value = undefined; // 缓存值
_getter; // getter 函数
/**
* 构造函数
* @param {Function} getter - getter 函数
*/
constructor(getter) {
this._getter = getter;
}
/**
* 执行 getter 并返回结果
* @returns {any} getter 执行结果
*/
get value() {
return (this._value = this._getter());
}
}
对象属性的处理
当 source 是对象时,会交给 propertyToRef 函数处理:
js
/**
* 将对象属性转换为 ref 的内部函数
* @param {object} source - 源对象
* @param {string|number} key - 属性键
* @param {any} defaultValue - 默认值
* @returns {ObjectRefImpl} 对象属性 ref
*/
function propertyToRef(source, key, defaultValue) {
const val = source[key];
// 如果属性值已经是 ref,直接返回;否则创建对象属性 ref
return isRef(val) ? val : new ObjectRefImpl(source, key, defaultValue);
}
如果 source 中属性的值已经是 Ref,则直接返回;否则继续交给 ObjectRefImpl 处理:
js
/**
* 对象属性引用实现类
* 用于将对象的某个属性转换为 ref
*/
class ObjectRefImpl {
[ReactiveFlags.IS_REF] = true; // 标识为 ref 对象
_object; // 源对象
_key; // 属性键
_defaultValue; // 默认值
/**
* 构造函数
* @param {object} object - 源对象
* @param {string|number} key - 属性键
* @param {any} defaultValue - 默认值
*/
constructor(object, key, defaultValue) {
this._object = object;
this._key = key;
this._defaultValue = defaultValue;
}
/**
* 获取属性值
* @returns {any} 属性值或默认值
*/
get value() {
const val = this._object[this._key];
return (this._value = val === undefined ? this._defaultValue : val);
}
/**
* 设置属性值
* @param {any} newValue - 新值
*/
set value(newValue) {
this._object[this._key] = newValue;
}
}
手动实现toRefs函数
toRefs 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
js
/**
* 将响应式对象的所有属性转换为 refs
* @param {object|Array} object - 响应式对象
* @returns {object} 包含所有属性 refs 的对象
*/
export function toRefs(object) {
// 根据源对象类型初始化返回对象
const ret = isArray(object) ? new Array(object.length) : {};
// 遍历对象的所有可枚举属性(包括继承的属性)
for (const key in object) {
ret[key] = propertyToRef(object, key);
}
return ret;
}
总结
通过手动实现 ref、toRef 和 toRefs,我们深入理解了 Vue3 响应式系统中引用类型的工作原理。这些实现展示了 Vue3 如何通过巧妙的类设计和依赖收集机制,为开发者提供简洁而强大的响应式 API。
理解这些底层原理不仅有助于我们更好地使用 Vue3,也能在遇到复杂场景时提供解决问题的思路。希望这篇文章能帮助你更深入地掌握 Vue3 的响应式系统!