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函数直接操作原来的数据即可。
相关推荐
LvManBa39 分钟前
Vue学习记录之六(组件实战及BEM框架了解)
vue.js·学习·rust
200不是二百44 分钟前
Vuex详解
前端·javascript·vue.js
LvManBa1 小时前
Vue学习记录之三(ref全家桶)
javascript·vue.js·学习
深情废杨杨1 小时前
前端vue-父传子
前端·javascript·vue.js
工业互联网专业2 小时前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
J不A秃V头A2 小时前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂3 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客3 小时前
pinia在vue3中的使用
前端·javascript·vue.js
天下无贼!5 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
爱喝水的小鼠6 小时前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js