vue3源码-ref的实现

ref的实现与reactive类似,但不同之处在于ref既可以传入对象也可以传入简单类型的数据,那么vue采取了什么方法监听简单类型数据呢?那么带着这个疑问开始今天的源码学习吧。

构建ref函数处理复杂数据类型能力

ts 复制代码
export interface Ref<T = any> {
  value: T
}


// 暴露出去的方法,将具体的内部实现交给createRef方法
export function ref(value: unknown) {
  return createRef(value, false)
}

// 核心作用:创建一个RefImpl实例
export function createRef(rawValue: unknown, shallow: boolean) {
  // 判断传入的值是否经过ref处理过
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

// 判断数据是否已经被处理过,是 RefImpl 实例
export function isRef(r: any): r is Ref {
  // 判断传入的值是否存在__v_isRef === true的属性,如果有说明是 RefImpl 实例
  return !!(r && r.__v_isRef === true)
}

首先创建用于暴露出去的方法ref,它只需要返回createRef函数的返回值即可,将具体实现隐藏在模块内部。

createRef函数接收两个参数,rawValue表示ref函数传入的被监听数据,shallow是一个布尔值,用于判断是否为简单类型,true则为简单类型,false则为复杂类型。

createRef函数的目的是返回一个RefImpl实例,为什么要返回这个实例在下文详细解释,总之createRef函数中通过判断传入的值是否已经经过ref函数的处理,从而决定是直接返回还是进入生成RefImpl实例的逻辑。

RefImpl

ts 复制代码
export class RefImpl<T> {
  // 私有属性_value存放传入的value值
  private _value: T
  // 公有属性dep存放依赖关系,默认是undefined
  public dep?: Dep = undefined
  // 对实例进行标记,表示这个实例已经被ref函数处理过了
  public readonly __v_isRef = true
  // 构造器
  constructor(value: T, public readonly __v_isShallow) {
    // 对value进行类型判断,shallow为true表示传入的是简单类型数据,否则是复杂类型数据
    this._value = this.__v_isShallow ? value : toReactive(value)
  }
  
  // 核心逻辑
  get value() {
    trackRefValue(this)
    return this._value
  }
  set(neVal) {}
}

// 收集依赖
export function tractValue(ref) {
  if (activeEffect) {
    return tractEffects(ref.dep || (ref.dep = createDep()))
  }
}

生成一个RefImpl实例需要两个参数valueshallowvalue在经过基于shallow的类型判断后,决定返回处理过的值或未处理的值,在这一部分我们主要关注复杂类型的响应性,所以value需要通过reactive函数的处理,返回具有响应性的Proxy。这也解释了ref中传入复杂数据的响应性来源,ref得以处理复杂数据的能力主要是reactive函数提供的。

toReactive函数

明确了ref处理复杂类型数据的能力是来源于reactive函数,那么就可以在reactive模块中定义toReactive函数用于转化数据为proxy

ts 复制代码
// 判断是否为对象
function isObject (value: unknown) {
  return value !== null && typeof value === 'object'
}

// 如果是对象则返回经reactive函数处理的proxy,否则直接返回原值 
export const toReactive <T extends unknown >(value: T) => {
  return isObject(value) ? reactive(value as object) : value
}

核心逻辑

为什么经过ref处理的数据在访问时需要添加.value,在这一部分就可以解释了,原因是对数据的访问由RefImpl实例对象代理了。在ref中传入的值作为参数被保存在实例对象的私有属性_value中,.value的本质就是在调用实例对象的gettersetter,在get操作中无论传入的数据是什么类型都不影响,因此我们先讨论get的实现。

get函数被触发时需要做两件事:

  1. 收集数据对应的执行函数作为依赖。这一步的大部分操作都已经在reactive模块中完成,我们只需要调用其中的trackEffects函数收集存储在dep属性中的执行函数组成的Set类型数据,如果dep属性是undefined就调用createDep函数新生成一个。
  2. 返回私有属性_value的值。

总结

到这里关于ref函数处理复杂数据类型的流程就走完了,可以看出ref在处理复杂数据类型的能力上很大程度上是依靠reactive函数提供的,本质上还是通过Proxy代理对象的属性访问,从而在gettersetter中收集依赖和触发依赖。但是ref函数和reactive函数还是存在一些不同之处的:

ref

  1. ref函数返回的是RefImpl实例对象。
  2. 想要访问ref处理后的值需要通过.value,通过实例对象的gettersetter得到存储在_value中的proxy实例。

reactive

  1. reactive可以直接返回proxy实例

构建ref函数处理简单类型数据能力

ts 复制代码
export class RefImpl<T> {
  // 省略
  ...
  
  // 增加rawValue属性保存传入的原始值,可在set操作时用于比较
  private _rawValue: T
  constructor(value: T, __v_isShallow: boolean) {
    this._value = __v_isShallow ? value : toReactive(value)
    // 直接保存原始值
    this._rawValue = value
  }
  
  // setter
  set value(newVal) {
    // 判断新旧值是否存在不同
    if (hasChange(newVal, this._rawValue)) {
      // 先更新原始值
      this._rawValue = newVal
      // 更新处理后的访问值
      this._value = toReactive(newVal)
      // 触发依赖
      triggerRefValue(this)
    }
  }
}
// 判断两个值是否不同
const hasChange = (newVal: any, oldVal: any):boolean => {
  return !Object.is(newVal, oldVal)
}

// 触发依赖的函数
export function triggerRefValue(ref: any) {
  if (ref.dep) {
    triggerEffects(ref.dep || (ref.dep = createDep()))
  } 
}

从这段代码中可以看出RefImpl实例的setter是怎么处理数据更新的:在实例对象中使用_rawValue私有属性存储不加修改的原始值,然后在每次set操作时根据原始值和新值判断set操作是否存在数据的更新,同时在数据更新的过程中分开处理更新原始值_rawValue和实际访问属性_value的值。最后如果触发dep属性中保存的依赖。

ref函数处理简单类型数据不是通过reactive创建proxy代理对象,而是生成RefImpl实例对象,通过getset标记value方法,让.value属性被调用的同时主动触发value方法完成依赖收集依赖触发

总结

ref函数和reactive函数都具有给数据添加响应性的能力,但它们在使用和原理上都存在不同。

  1. ref既可以处理简单数据类型,也可以处理复杂数据类型;reactive函数只能处理复杂类型数据。
  2. ref函数的本质是生成RefImpl实例对象,通过getset标记value方法,达到对value属性的访问拦截的目的;reactive函数的本质是生成proxy代理对象,通过代理对象监听源数据。
  3. ref函数的响应性需要通过.value来保证,而reactive函数直接操作原来的数据即可。
相关推荐
碎碎念_4922 小时前
SpringBoot + Vue 前后端分离从 0 到 1 完整环境配置流程
vue.js·spring boot·后端
shuaijie05183 小时前
强制修改调用接口的api地址。
javascript·vue.js·ecmascript
katiua7 小时前
Vue-i18n TypeScript 补全提示
vue.js
Fuzio7 小时前
用 Spring Boot + Vue + Fuzio 构建现代 Java 桌面应用
vue.js·spring boot
kisshyshy7 小时前
从0到1彻底理解流式输出:先读懂Vue的“乐高积木”,再拧开LLM的“数据水龙头”
javascript·vue.js·人工智能
饮茶三千7 小时前
电子保函模板编辑器实战二:HTML→FTL 编译转换的设计与实现
前端·vue.js·设计模式
英勇无比的消炎药7 小时前
表单筛选利器:TinyVue BaseSelect 基础选择器实战
vue.js
先吃饱再说8 小时前
从零理解 Vue 3:组件化、数据绑定与响应式完全指南
vue.js·前端框架
gis开发之家10 小时前
《Vue3 从入门到大神29篇》单元测试与 E2E 测试 —— 保障 Vue3 项目的质量
前端·javascript·vue.js·单元测试·前端框架·vue3
forever_world10 小时前
# 💧 从前端视角,彻底搞懂 AI 流式输出 —— 从"接根水管"到 Vue 3 实战
前端·vue.js