Vue3.2 响应式原理解析(三)

1,815 阅读5分钟

2.jpg

前言

这篇文章的内容是vue另一个常见重要的API ref 实现的思路是有一个容器存储的值,有两个方法,一个用于调用 trackEffect 收集依赖,一个用于调用 triggerEffect 触发依赖更新,还有很多衍生API,例如:toRef,toRefs等

源代码位置在:vue-next3.2/packages/reactivity/src/ref.ts

工具函数

// 如果用户使用ref包装对象,使其转换成reactive代理对象
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
  
// 判断是否是ref对象
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}

// 转换成原始数据
export function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

基本了解

代码中有主要功能都是基于三个类进行实现的,分别是:RefImplCustomRefImplObjectRefImpl

RefImpl:ref api的构造函数,

CustomRefImpl:自定义ref customRef的构造函数

ObjectRefImpl: toRef的构造函数 可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。

源码解析

ref API

这个方法的作用是:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value

看看实现原理

export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
  // 逻辑封装在 createRef 中
  return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  // RefImpl 实例化一个 ref
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  // 存储的响应式的值
  private _value: T
  // 存储的是原始数据
  private _rawValue: T

  // 依赖项
  public dep?: Dep = undefined
  // 标识为他是ref
  public readonly __v_isRef = true

  // 在初始化时,对 _value _rawValue进行赋值 但是这两个数据不能直接被外面获取和修改
  // 有一对getter和setter来进行拦截
  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }

  get value() {
    // 获取值时、通过trackRefValue做一些处理之后,再去调用trackEffects进行依赖收集
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    // value可能是代理对象,一般不会存储代理对象而是原始数据
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      // 原始数据可能是对象,转换一下
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

从上面可以看出,ref的核心逻辑都在 RefImpl 这个类里面,其中有四个重要的属性,_value:取数据和修改数据都是在这个属性上面操作,_rawValue:存储的是原始数据,在_value被修改的时候,他也会跟着被修改,dep:存储的是副作用函数,一般触发依赖都是遍历这个集合,_v_isRef:这个属性被设置为true 标识这个对象是ref类型

toRef

这个API是ref的变体,ref是把一个值变成响应式的值,而这个是拿到对象上的一个属性的属性值变成响应式属性作为一个ref,下面看一下代码实现

export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): ToRef<T[K]> {
// 是ref对象 返回 不是就实例化
  return isRef(object[key])
    ? object[key]
    : (new ObjectRefImpl(object, key) as any)
}

class ObjectRefImpl<T extends object, K extends keyof T> {
  // __v_isRef 为true 作为ref对象的标识
  public readonly __v_isRef = true

  // 存储对象 存储key
  constructor(private readonly _object: T, private readonly _key: K) {}

  // 获取和修改都是在对象上操作 ref对象本身不会存储值
  get value() {
    return this._object[this._key]
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}

这样所产生的ref对象,身上不会存储值,而是存储了一个目标对象,且都有一个标识__v_isRef 作为ref标识,但是,在获取值和修改值是去目标对象上操作,不会有收集依赖和触发依赖的操作,而toRefs 就是由这个API所衍生处理的,

toRefs

其核心就是遍历目标,然后拿到所有的key和value去调用toRef 返回值存储在一个相对于的结构对象上,最后再把对象返回,但前提是目标是一个代理对象,

export function toRefs<T extends object>(object: T): ToRefs<T> {
  if (__DEV__ && !isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  // 根据源数据结构生成一个对应的数据结构
  const ret: any = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    // 存储转换后的数据
    ret[key] = toRef(object, key)
  }
  return ret
}

自定义Ref

官方文档是这样说的:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。

class CustomRefImpl<T> {
  public dep?: Dep = undefined

  private readonly _get: ReturnType<CustomRefFactory<T>>['get']
  private readonly _set: ReturnType<CustomRefFactory<T>>['set']

  public readonly __v_isRef = true

  constructor(factory: CustomRefFactory<T>) {
    // factory 是用户传递进来的工厂函数
    // get 和 set 是用户自定义 并将依赖收集和依赖触发函数传递过去
    const { get, set } = factory(
      () => trackRefValue(this),
      () => triggerRefValue(this)
    )
    // 存储自定义 get 和 set
    this._get = get
    this._set = set
  }

  get value() {
    return this._get()
  }

  set value(newVal) {
    this._set(newVal)
  }
}

// 自定义Ref
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
  return new CustomRefImpl(factory) as any
}

具体原理是,内部接受一个函数,函数需要实现两个方法并且返回,而函数会接受到收集依赖和触发依赖的方法,注意的一共由两点:1.需要自己去手动调用收集和触发依赖的方法,2. 在进行更新值的时候,内部不会进行校验是否由改变值,这可能会触发一些不必要的调用,需要自己去校验

总结

我个人认为ref模块是整一个响应式模块里最简单的,相对其他模块,不会由太复杂的逻辑,很容易看明白,而ref本身可以视为一个容器,里面存着一系列属性和一对更新方法,且对于vue3的组合API,ref的使用可能会比reactive更多,在传递的过程中,ref 传递的是容器的地址, reactive 传递只是通过get返回的值,虽然可以通过toRefs转换成,那还不如开始就用ref 以上就是我对ref的实现原理的解析,欢迎各位大佬补充,谢谢大家