Vue3 响应式原理:手写实现 ref 函数

前言

在 Vue3 的响应式系统中,ref 是一个核心 API,它让我们能够创建响应式的数据引用。虽然日常开发中我们频繁使用它,但你是否曾好奇过它的内部实现原理?今天,就让我们一起来揭开 ref 的神秘面纱,手动实现一个属于自己的 ref 函数!

系列文章

为了帮助你更好地理解 Vue3 响应式系统的完整实现,推荐阅读本系列的其他文章:

  1. Vue3 响应式原理:从零实现 Reactive
  2. Vue3 响应式原理:手写 Computed 和 Watch 的奥秘

💡 提示

本文中涉及的部分函数可能在其他文章中实现,你可以通过上述目录查找完整实现。如果想深入研究,也可以直接查看 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;
}

总结

通过手动实现 reftoReftoRefs,我们深入理解了 Vue3 响应式系统中引用类型的工作原理。这些实现展示了 Vue3 如何通过巧妙的类设计和依赖收集机制,为开发者提供简洁而强大的响应式 API。

理解这些底层原理不仅有助于我们更好地使用 Vue3,也能在遇到复杂场景时提供解决问题的思路。希望这篇文章能帮助你更深入地掌握 Vue3 的响应式系统!

相关推荐
码客前端3 分钟前
理解 Flex 布局中的 flex:1 与 min-width: 0 问题
前端·css·css3
Komorebi゛4 分钟前
【CSS】圆锥渐变流光效果边框样式实现
前端·css
工藤学编程16 分钟前
零基础学AI大模型之CoT思维链和ReAct推理行动
前端·人工智能·react.js
徐同保17 分钟前
上传文件,在前端用 pdf.js 提取 上传的pdf文件中的图片
前端·javascript·pdf
怕浪猫18 分钟前
React从入门到出门第四章 组件通讯与全局状态管理
前端·javascript·react.js
内存不泄露23 分钟前
基于Spring Boot和Vue 3的智能心理健康咨询平台设计与实现
vue.js·spring boot·后端
欧阳天风25 分钟前
用setTimeout代替setInterval
开发语言·前端·javascript
EndingCoder29 分钟前
箭头函数和 this 绑定
linux·前端·javascript·typescript
郑州光合科技余经理29 分钟前
架构解析:同城本地生活服务o2o平台海外版
大数据·开发语言·前端·人工智能·架构·php·生活
沐墨染31 分钟前
大型数据分析组件前端实践:多维度检索与实时交互设计
前端·elementui·数据挖掘·数据分析·vue·交互