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 的响应式系统!

相关推荐
合作小小程序员小小店1 小时前
web网页开发,在线%宠物销售%系统,基于Idea,html,css,jQuery,java,ssh,mysql。
java·前端·数据库·mysql·jdk·intellij-idea·宠物
荔枝吖1 小时前
html2canvas+pdfjs 打印html
前端·javascript·html
文心快码BaiduComate1 小时前
全运会,用文心快码做个微信小程序帮我找「观赛搭子」
前端·人工智能·微信小程序
合作小小程序员小小店1 小时前
web网页开发,在线%档案管理%系统,基于Idea,html,css,jQuery,java,ssh,mysql。
java·前端·mysql·jdk·html·ssh·intellij-idea
合作小小程序员小小店1 小时前
web网页开发,在线%物流配送管理%系统,基于Idea,html,css,jQuery,java,ssh,mysql。
java·前端·css·数据库·jdk·html·intellij-idea
三门1 小时前
web接入扣子私有化智能体
前端
林小帅1 小时前
AI “自动驾驶” 的使用分享
前端
起名时在学Aiifox2 小时前
深入解析 Electron 打包中的 EPERM: operation not permitted 错误
前端·javascript·electron
游戏开发爱好者82 小时前
Fiddler抓包工具完整教程 HTTPHTTPS抓包、代理配置与API调试实战技巧(开发者进阶指南)
前端·测试工具·ios·小程序·fiddler·uni-app·webview