Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue3-ref源码解析 #19

Open
bill-lai opened this issue Feb 18, 2022 · 0 comments
Open

vue3-ref源码解析 #19

bill-lai opened this issue Feb 18, 2022 · 0 comments

Comments

@bill-lai
Copy link
Owner

阅读准备

本文使用的vue版本为3.2.26。在阅读 ref 源码之前,我们需要知道它的特性,可以通过阅读单例测试源码或者是阅读官网的 API了解特性,推荐阅读单例,了解特性在后面阅读时才能更好理解。

  我们前两章讲讲解了reactive源码解析effect源码解析,并且知道了它们是如何实现响应式的,还没看过的小伙伴可以先阅读一下。我们回顾一下,reactive函数可以创建通过Proxy实现的响应式对象,响应式对象需要在effect中使用才能收集到依赖,在更改响应式对象时,代理会通过trigger通知所有依赖的effect对象,并执行effect的监听方法。

  因为reactive创建的响应式对象是通过Proxy来实现的,所以传入数据不能为基础类型,比如numberstringboolean。创建回来的对象必须保持它的引用,不能重新赋值,这样会失去响应式的特征,比如:

import { reactive, effect } from 'vue'

let reuser = reactive({ name: 'bill', sex: '男', age: 18 })
effect(() => {
  console.log(`${reuser.name}的性别为${reuser.sex},年龄为${reuser.age}岁`)
})

let change = { sex: '女', age: 19 }
// 更改了proxy的指向,失去响应式特征,并且在下一次垃圾回收会回收掉之前的proxy
reuser = {
  ...reuser,
  ...change
}

  上方代码为了合并修改项将两个对象展开到一个对象中,这样更改会丢失proxy的指向,失去响应式特征,并且在下一次垃圾回收会回收掉之前的proxy,所以只能够找到修改属性,在原来的proxy基础上一个一个修改。

  ref对象是对reactive不支持的数据的一个补充,让如基础数据响应式进行支持,以及更方便的对象替换操作推出的。下面我们先了解一下ref的特性。

  • 使用refshallowRef函数创建ref对象,ref通过value属性进行访问和修改传入参数。

  • reactive不同,ref的参数没有任何限制。

  • 使用reactive可接受的对象为ref参数对象时,isReactive(ref.value)true

  • refeffect监听函数中使用可响应式

  • refeffect中只有value属性是可响应式的

  • customRef可以创建自定义gettersetterref,创建时需要提供一个创建get, set工厂方法,工厂方法会传入收集方法和触发方法,由用户主动触发。如

    let value = 1
    const custom = customRef((track, trigger) => ({
      get() {
        track()
        return value
      },
      set(newValue: number) {
        value = newValue
        trigger()
      }
    }))
  • 使用toRef可以通过proxy的某个属性生成为可以有默认值的ref对象

  • 使用toRefs可以通过proxy的数据结构以及所有属性,生成与proxy数据结构一致的,所有属性值为ref对象的对象

  综合上面的特性和之前讲解effect的实现原理,能猜得到ref对象会对value属性的修改和获取时进行拦截,在valueget的时候收集依赖,在set的时候获取依赖关联的effect再触发依赖函数。ref对属性修改和获取时不能通过proxy来实现,ref支持基础类型而proxy不支持。收集依赖时不能使用effect文件中的targetMap关联effecttargetMapWeakMap类型,WeakMap类型仅支持对象作为key,不支持基础类型。

ref和shallowRef

  接下来我们看看refshallowRef的具体实现:

// 是否是ref根据属性的__v_isRef决定
export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true)
}


export function ref(value?: unknown) {
  return createRef(value, false)
}

export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

// 创建ref对象,传入raw和是否是shallow
function createRef(rawValue: unknown, shallow: boolean) {
  // 如果之前时ref则直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

  与reactive一样ref创建的响应式对象也分为是否是shallowref对象支持对value深度响应式,也就是说ref.value.a.b中的修改都能被拦截,shallowRef对象只支持对value值的响应式。

  refshallowRef函数都使用createRef来创建ref对象,只是参数的区别。创建的ref对象会附加__v_isRef属性来标识是否是ref对象。在创建ref对象之前会检查入参是否是ref如果是就直接返回入参参数。

  我们看到ref函数创建的真实对象是RefImpl,采用了class写法,将rawshallow作为构造函数,下面我们看看这个class的实现:

// Ref对象类
class RefImpl<T> {
  // 存放 reactive(raw) 后的proxy
  private _value: T
  // 存放 raw
  private _rawValue: T

  // 建立与effect的关系
  public dep?: Dep = undefined
  // 是否ref的标识
  public readonly __v_isRef = true

  // 构造,传入raw 和 shallow
  constructor(value: T, public readonly _shallow: boolean) {
    // 存储 raw 
    this._rawValue = _shallow ? value : toRaw(value)
    // 如果是不是shallow则 存储 reactive proxy 否则存储传入参数
    this._value = _shallow ? value : toReactive(value)
  }

  // getter value拦截器
  get value() {
    // track Ref 收集依赖
    trackRefValue(this)
    return this._value
  }

  // setter value拦截器
  set value(newVal) {
    // 如果是需要深度响应的则获取 入参的raw
    newVal = this._shallow ? newVal : toRaw(newVal)

    // 查看要设置值是否与当前值是否修改
    if (hasChanged(newVal, this._rawValue)) {
      // 存储新的 raw
      this._rawValue = newVal
      // 更新value 如果是深入创建的还需要转化为reactive代理
      this._value = this._shallow ? newVal : toReactive(newVal)
      // 触发value,更新关联的effect
      triggerRefValue(this, newVal)
    }
  }
}

  如果不是shallow传入的value会通过toReactive转化为reactive,然后存在ref._value中。在get的时候直接返回这个reactive,这就是使用reactive可接受的对象为ref参数对象时,isReactive(ref.value)true的原因,也是为什么能深度响应的原因。

  ref还会存储入参和set的原始值,如果不是shallow则通过toRaw获取,存储在_rawValue属性中,存储这个值是为了能正确的判断值是否被修改。所以下方这种情况是不会调用triggerRefValue的,因为原始值是一样的。

const target = { name: 'bill' }
const reTarget = reactive(target)
const targetRef = ref(reTarget)

targetRef.value = target

  ref对象还有个非常重要的属性dep,这里的dep与我们之前在effect一章讲的dep是一样的,没看过的同学可以去看一下。我们简单回顾一下。

  reactive对象是通过targetMapDep关联的。reactive收集时通过track函数获取dep,然后通过dep对象调用trackEffects函数来将effectDep关联。

  reactive触发时通过trigger函数整理相关联的多个dep最终合并成一个dep,然后通过dep调用triggerEffects获取关联的effect收集函数并触发。

  dep中的具体细节管理是通过trackEffects函数和effect对象管理的,将depeffect是由trackEffects函数处理的, 触发是由triggerEffects函数执行的。

  也就是说基于现有effect的基础上,创建响应式对象只需要收集时获取dep并调用trackEffects(dep), 触发时获取收集时的dep并调用triggerEffects(dep)dep属性就是ref能成为响应式对象的根本原因。

  接下来我们看看ref是如何实现trackEffects(dep)triggerEffects(dep)的。refget value时会调用trackRefValue,在set value时,如果value值发生了更改则调用triggerRefValue。可以猜到这两个方法就是实现响应式的关键,接下来我们看看他们的具体实现

// 收集 ref 依赖 调用trackEffects(dep)
export function trackRefValue(ref: RefBase<any>) {
  // 如果当前开启了跟踪
  if (isTracking()) {
    // 获取raw ref数据
    ref = toRaw(ref)
    // 如果当前ref还未初始化dep则创建
    if (!ref.dep) {
      ref.dep = createDep()
    }
    // 如果是开发环境,则传入track细节, 
    if (__DEV__) {
      trackEffects(ref.dep, {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep)
    }
  }
}

// 触发 ref 调用trackEffects(dep)
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  // 获取raw ref数据
  ref = toRaw(ref)
  // 如果当前ref 有关联的dep
  if (ref.dep) {
    // 如果当前是开发环境则发送具体触发细节
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        // SET引起的变化
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

  trackRefValue会在适当的时候初始化dep并调用trackEffectstriggerRefValue会获取refdep并调用triggerEffects,就是我们上面说的内容。

  大家注意到传入的ref会调用toRaw方法来重新赋值,这个方法是获取reactive的原始数据的。因为用户可能使用reactive(ref(raw))来获取数据,如果直接使用可能会收集到dep属性的依赖。另外大家思考一下下面这段代码的effect监听函数会触发几次?

const countRef = ref(0)
const reCount = reactive(countRef)

effect(() => {
  console.log(reCount.value)
})

reCount.value = 3

  答案是四次,第一次是首次收集依赖,reactive会收到value的获取,存储value属性的dep附加到targetMap。然后调用ref.valueref在获取value时会调用trackRefValue,创建dep附加到自身属性上。注意ref.value返回this._value,这时候reactive收到_value属性的获取,存储_value属性的dep,附加到targetMap中。所以创建了三个dep。当发生更改新值存储到ref._value中,而对于reactive来说value_value是完全没关联的所以会触发两次,而ref自身会触发一次没所以一共是四次。

customRef

  接下来我们看看自定义ref方法customRef是如何实现的:

// 自定义ref对象类
class CustomRefImpl<T> {
  // 依赖dep 存储effets
  public dep?: Dep = undefined

  // 缓存getter setter
  private readonly _get: ReturnType<CustomRefFactory<T>>['get']
  private readonly _set: ReturnType<CustomRefFactory<T>>['set']

  // mark ref
  public readonly __v_isRef = true

  // 传入ref工厂函数
  constructor(factory: CustomRefFactory<T>) {
    // 构建getter setter,传入track trigger函数
    const { get, set } = factory(
      () => trackRefValue(this),
      () => triggerRefValue(this)
    )
    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
}

  customRef会创建CustomRefImpl的一个实例并返回,CustomRefImpl的实现和Ref差不多,使用trackRefValuetriggerRefValuedepeffect关联实现响应式。不过CustomRefImpl会在工厂函数中传入trackRefValuetriggerRefValue,将收集依赖和触发执行权交给用户。让用户在适当的时候调用。在使用value时候调用生产的get方法,在设置value是调用生产的set方法。一般是在get的时候调用收集函数,set的时候触发函数。

toRefs和ObjectRefImpl

  我们在使用reactive时通过缓存属性值很可能会失去响应式特性。因为属性值可能是reactive不支持深入响应的值,这时候缓存属性值,或者是通过ES6解构出来的值是不具备响应特性的。比如在下面这两种使用方式:

const reuser = reactive({ name: 'bill', sex: '男' })
const { name } = reuser
const sex = reuser.sex

  这样的话就得一直使用reuser.name的方式来进行访问,vue有两个api能很好的解决这个问题,就是toReftoRefs

  toRef通过reactive代理和代理的某个属性生成为ref并且可以携带默认值。而toRefs根据reactive代理生成所有属性值为ref的对象。生成的refvalue是代理属性值的映射,两端更改都会实时同步,我们看看是如何使用的:

const reuser = reactive({ name: 'bill', sex: '男' })
const name = toRef(reuser, 'name', '未命名')
const { sex } = toRefs(reuser)

console.log(reuser.name, reuser.sex)  //bill 男
console.log(name.value, sex.value)    //bill 男

name.value = 'lzb'
sex.value = '女'

console.log(reuser.name, reuser.sex)  //lzb 女
console.log(name.value, sex.value)    //lzb 女

delete reuser.name

console.log(reuser.name, reuser.sex)  //undefined 女
console.log(name.value, sex.value)    //未命名 女

  接下来我们看看toReftoRefs的具体实现:

// 将proxy对象和目标的属性 转化为ref 并拥有默认值
class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(
    // 记录代理
    private readonly _object: T,
    // 要辅助的key
    private readonly _key: K,
    // 默认值
    private readonly _defaultValue?: T[K]
  ) {}

  // 获取value
  get value() {
    const val = this._object[this._key]
    return val === undefined ? (this._defaultValue as T[K]) : val
  }

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

// 将proxy对象和目标的属性 转化为ref 并拥有默认值
export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue?: T[K]
): ToRef<T[K]> {
  const val = object[key]
  return isRef(val)
    ? val
    : (new ObjectRefImpl(object, key, defaultValue) as any)
}

// 将proxy对象所有属性转化为ref值
export function toRefs<T extends object>(object: T): ToRefs<T> {
  // 只有内部代理才能toRefs
  if (__DEV__ && !isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  // 分别对所有属性toRef
  const ret: any = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

  toRef会先查看proxy[key]是否是ref如果是的话直接返回,如果不是则创建ObjectRefImpl并且将参数传入,ObjectRefImpl会标识当前对象是ref类型 (通过__v_isRef属性) ,并且缓存proxykey和默认值。get value时直接通过proxy[key]来获取并返回,如果回去的值是undefined则使用默认值。set value时则通过proxy[key] = newVal来设置。

  toRefs则是将每个属性都调用一次没有默认值的toRef,并且返回与proxy一致的数据结构。

  为什么这里的ObjectRefImpl类不需要dep属性和收集依赖和触发更改呢?这是因为_object属性本身是proxy类型,当我们在使用proxy[key]就实现了收集依赖,在proxy[key] = newVal是就触发了更改。

其他辅助方法

  ref文件中还声明了其他辅助方法,比如triggerRef手动触发ref的更改使关联的effect重新执行收集函数;unref获取ref的原始值。这两个方法比较简单直接看源码即可,这里就不再讲解了。

// 手动触发 ref
export function triggerRef(ref: Ref) {
  // 开发环境用当前值做最新值变化
  triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}

// 解构ref,直接返回value
export function unref<T>(ref: T | Ref<T>): T {
  return isRef(ref) ? (ref.value as any) : ref
}

  还有一个辅助方法proxyRefs,这个方法将一个对象直属属性内的所有ref属性值解构访问 (不需要通过value下标访问) 。什么是直属属性就是第一层属性,比如下方的代码:

const name = ref('bill')
const unUser = proxyRefs({
  name: name,
  adderss: {
    city: ref('珠海')
  }
})

console.log(unUser.name)  // bill
console.log(unUser.address.city)  // Ref

unUser.name = 'lzb'

console.log(unUser.name)  // lzb 

proxyRefs只对第一层属性的ref解构。我们看看它的源码:

// 浅解构ref处理器
const shallowUnwrapHandlers: ProxyHandler<any> = {
  // getter将unref方便访问
  get: (target, key, receiver) => {
    return unref(Reflect.get(target, key, receiver))
  },
  // setter先查看是否是ref,如果是则更新value
  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    // 如果新数据不是ref但旧数据是则更新value
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      return Reflect.set(target, key, value, receiver)
    }
  }
}

// 创建代理ref 解构方便使用
export function proxyRefs<T extends object>(
  objectWithRefs: T
): ShallowUnwrapRef<T> {
  // 如果是reactive对象则无需解构
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

  当proxyRefs入参是reactive对象时则直接返回,reactive对象本身会对ref解构,而且是深度的,这里就不需要处理。有个特殊情况shallowReactive对象不会对ref解构,但是也直接返回了,也就是说这个方法对shallowReactive对象时无效的!

  如果proxyRefs入参不是reactive对象,则创建代理,get拦截器通过unref来获取值返回,set拦截器通过判断当前要更新的是否是ref如果是则更新value

  到这里我们ref的所有内容就已经讲完了,接下来日常小结。

小结

  • ref对象自身附加了dep,在收集依赖时通过trackEffects函数,触发时通过triggerEffects函数
  • ref能够创建深度响应式是依赖了reactive
  • Proxy代理对象可以通过toReftoRefs辅助方法保持对单个属性的引用,赋值修改会映射到Proxy
  • customRef函数可以创建自由度极高的响应式对象

上一章:vue3-effect源码解析

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant