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函数直接操作原来的数据即可。
相关推荐
rookie fish几秒前
Electron+Vite+Vue项目中,如何监听Electron的修改实现和Vue一样的热更新?[特殊字符]
前端·vue.js·electron
上优30 分钟前
Vue3纯前端同源跨窗口通信移动AGV小车
前端·vue.js·状态模式
一只小阿乐31 分钟前
vue-router 的实现原理
前端·javascript·vue.js·路由·vue-router
小圣贤君31 分钟前
小说写作中的时间轴管理:基于 Vue 3 的事序图技术实现
vue.js·electron·写作·甘特图·时间轴·事序图
Zz_waiting.31 分钟前
案例开发 - 日程管理 - 第七期
开发语言·前端·javascript·vue.js·html·路由
一只小风华~35 分钟前
Vue:事件处理机制详解
前端·javascript·vue.js·typescript·前端框架
dy17175 小时前
element-plus表格默认展开有子的数据
前端·javascript·vue.js
索迪迈科技9 小时前
网络请求库——Axios库深度解析
前端·网络·vue.js·北京百思可瑞教育·百思可瑞教育
一只小风华~11 小时前
Vue: Class 与 Style 绑定
前端·javascript·vue.js·typescript·前端框架
切糕师学AI12 小时前
前后端分离架构中,Node.js的底层实现原理与线程池饥饿问题解析
前端·vue.js·node.js