Vue 3.x 响应式原理——ref源码分析

4,815 阅读5分钟

在上一篇文章Vue 3.x 响应式原理——reactive源码分析中,笔者简述了Vue 3.x 的 reactive API 的实现原理,了解过 Vue Composition API 的同学都知道reactiveref创建响应式数据的区别,本文通过讲述ref API 的实现原理,帮助更进一步了解 Vue 3.x 的响应式原理。

阅读此文之前,如果对以下知识点不够了解,可以先了解以下知识点:

笔者之前也写过相关文章,也可以结合相关文章:

Ref对象

ref的作用是提供响应式包装对象,便于利用Vue Composition API 进行函数式的组装,首先我们通过isRef函数入口,看看Vue 3.x 是如何标识ref对象的:

const isRefSymbol = Symbol()

export interface Ref<T = any> {
  // This field is necessary to allow TS to differentiate a Ref from a plain
  // object that happens to have a "value" field.
  // However, checking a symbol on an arbitrary object is much slower than
  // checking a plain property, so we use a _isRef plain property for isRef()
  // check in the actual implementation.
  // The reason for not just declaring _isRef in the interface is because we
  // don't want this internal field to leak into userland autocompletion -
  // a private symbol, on the other hand, achieves just that.
  [isRefSymbol]: true // 用一个symbol来标识ref对象,但是后面又被改成了通过_isRef属性来标识
  value: UnwrapRef<T> // 响应式包装对象的value属性,是解包装的值
}
// ...

export function isRef(r: any): r is Ref {
  // 通过_isRef属性判断一个对象是否是ref对象
  return r ? r._isRef === true : false
}

看上面代码,我们可以认识到,ref对象总会被挂载一个叫做_isRef的属性,所以通过_isRef这个属性是否存在就可以帮助我们判断一个对象是否是ref对象。

此外,ref对象含有一个属性叫valuevalue的类型是UnwrapRef<T>,下面看看UnwrapRef<T>

export interface ComputedRef<T = any> extends WritableComputedRef<T> {
  readonly value: UnwrapRef<T>
}

type UnwrapArray<T> = { [P in keyof T]: UnwrapRef<T[P]> }

// Recursively unwraps nested value bindings.
// 递归获取包装对象value的类型
// 因为ref不能是嵌套的ref,即value不能是一个ref对象
export type UnwrapRef<T> = {
  // 如果遇到value是computedRef类型,解套求其value的类型
  cRef: T extends ComputedRef<infer V> ? UnwrapRef<V> : T
  // 如果遇到value是Ref类型,解套求其value的类型
  ref: T extends Ref<infer V> ? UnwrapRef<V> : T
  // 如果遇到value是数组,对数组里每一项解包装
  array: T extends Array<infer V> ? Array<UnwrapRef<V>> & UnwrapArray<T> : T
  // 如果遇到value是对象,对对象每一项遍历解包装
  object: { [K in keyof T]: UnwrapRef<T[K]> }
}[T extends ComputedRef<any>
  ? 'cRef'
  : T extends Ref
    ? 'ref'
    : T extends Array<any>
      ? 'array'
      : T extends Function | CollectionTypes
        ? 'ref' // bail out on types that shouldn't be unwrapped
        : T extends object ? 'object' : 'ref']

通过上面的代码我们看到ref响应式包装对象的value的类型一定是一个解包装的对象,而不能是嵌套的ref。对于数组和对象类型,需要对其进行遍历,保证其中每项都没有嵌套ref对象,如果有嵌套的情况,需要再进行解包装。

ref

下面来看ref函数,ref函数将一个普通对象转化为响应式包装对象:

export function ref<T extends Ref>(raw: T): T
export function ref<T>(raw: T): Ref<T>
export function ref<T = any>(): Ref<T>
export function ref(raw?: unknown) {
  // 已经是ref对象了,直接返回原始值
  if (isRef(raw)) {
    return raw
  }
  // 转化为ref对象
  raw = convert(raw)
  const r = {
    _isRef: true,
    get value() {
      // getter触发时,触发依赖收集,源码在effect部分,在笔者下一篇文章会有讲述
      track(r, OperationTypes.GET, 'value')
      return raw
    },
    set value(newVal) {
      // setter触发时,首先调用convert转化为ref对象
      raw = convert(newVal)
      // trigger通知deps,通知依赖这一状态的对象更新
      trigger(
        r,
        OperationTypes.SET,
        'value',
        __DEV__ ? { newValue: newVal } : void 0
      )
    }
  }
  return r
}

// convert的作用是创建响应式包装对象,这里直接使用reactive,其原理在上一节有讲过
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

通过ref函数,我们了解到了ref的底层就是reactiveref对象具有对应的 getter 和 setter ,getter总是返回经过convert转化后的响应式对象raw,并触发 Vue 的依赖收集,对ref对象赋值会调用settersetter调用会通知deps,通知依赖这一状态的对象更新,并重新更新rawraw被保存为新的响应式包装对象。

toRefs

最后我们来看toRefstoRefsreactive对象转换为普通对象,其中结果对象上的每个属性都是指向原始对象中相应属性的ref引用对象,这在组合函数返回响应式状态时非常有用,这样保证了开发者使用对象解构或拓展运算符不会丢失原有响应式对象的响应。

export function toRefs<T extends object>(
  object: T
): { [K in keyof T]: Ref<T[K]> } {
  const ret: any = {}
  // 遍历对象的所有属性,都对其调用toProxyRef
  for (const key in object) {
    ret[key] = toProxyRef(object, key)
  }
  return ret
}

// toProxyRef相当于把对象的每个属性都变成一个包装对象,这样在结构和使用拓展运算符时,就不会丢失原有响应式对象的引用了
function toProxyRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return {
    _isRef: true,
    get value(): any {
      return object[key]
    },
    set value(newVal) {
      object[key] = newVal
    }
  } as any
}

小结

本文介绍了 Vue 3.x 的Ref对象的原理,ref的作用是便于我们使用 Vue 3.x 进行组件组合时,通过函数传参不会丢失响应式对象的原始引用,其原理的核心是包装和解包装,包装时,我们保证ref对象的value不能嵌套ref对象,所以使用了UnwrapRef,同时,对于对象和数组,包装和解包装时需要对其中每项进行遍历,以保证不会出现嵌套ref对象的情况。

后面笔者会对effect模块进行分析,讲述 Vue 3.x 在依赖收集原理。