Vue3源码浅析之 Ref

2,091 阅读4分钟

Ref 是Vue 3中引入的一个“新”概念,其设计目的是为了使响应式数据能以变量的形式传递。借用Composition API RFC 中的例子:

// 组合函数
function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0
  })

  // ...
  return pos
}

export default {
  setup() {
    // 模板中使用x、y丢失了响应性
    const { x, y } = useMousePosition()
    return {
      x,
      y
    }

    // 模板中使用x、y丢失了响应式
    return {
      ...useMousePosition()
    }

    // 只能通过这种包裹对象的形式获得响应式,模板中使用pos.x、pos.y
    return {
      pos: useMousePosition()
    }
  }
}

可以看出,在没有Ref的情况下,对于基本数据类型、进行解构或者使用扩展运算符得到的值,都会失去其原本的响应式特性。换句话说,就是经过这些操作后得到的值无法同步原始数据的变化。如果想要保持这种响应式,就只能使用包裹对象的方式。而Ref就像是一个容器,让我们能够以引用的方式传递任意类型值,并且这些值是具有响应式的。现在我们就一起来看看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  
  value: UnwrapRef<T>
}

Ref这个类型有两个字段,一个是为了跟普通对象做区分的symbol类型标识(实际上是用_isRef字段),另一个是值为UnwrapRef类型的value字段,顾名思义,value字段保存的是Ref对象层层拆包后的真实值。UnwrapRef类型定义如下:

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

// 递归地拆解出嵌套的类型
export type UnwrapRef<T> = {
  // 如果是ComputedRef类型,则递归拆解,否则保持原类型
  cRef: T extends ComputedRef<infer V> ? UnwrapRef<V> : T
  // 如果是Ref类型,则递归拆解,否则保持原类型
  ref: T extends Ref<infer V> ? UnwrapRef<V> : T
  // 如果是Array类型,则遍历key拆解为Array<UnwrapRef<V>>和UnwrapArray<T>的交叉类型,否则保持原类型
  array: T extends Array<infer V> ? Array<UnwrapRef<V>> & UnwrapArray<T> : T
  // 如果是Object类型,则遍历key,递归拆解其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']

// ComputedRef引自computed.ts,与Ref类型的区别在于其value属性为readonly的UnwrapRef
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
  readonly value: UnwrapRef<T>
}
export interface WritableComputedRef<T> extends Ref<T> {
  readonly effect: ReactiveEffect<T>
}

说白了,UnwrapRef类型会将object、array对象的键值进行层层拆包直至分解至基本的Ref类型,但是对于Ref、Function、Map、Set、WeakMap和WeakSet,则将它们视同于基本数据,不做进一步处理。

// 判断如果值为对象先将其转化为响应式对象,否则保持原值
const convert = <T extends unknown>(val: T): T =>  
  isObject(val) ? reactive(val) : val
// 根据 _isRef 字段判断是否为Ref类型
export function isRef(r: any): r is Ref {
  return r ? r._isRef === true : false
}
// 函数重载
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
  }
  // 转化原值  
  raw = convert(raw)  
  const r = {    
    _isRef: true, // 类型标识    
    get value() {      
      track(             // 依赖收集的过程 
        r, 
        OperationTypes.GET, 
        'value'
      )       
      return raw    
    },    
    set value(newVal) {      
      raw = convert(newVal)      
      trigger(           // 触发更新的过程
        r,
        OperationTypes.SET,
        'value',
        __DEV__ ? { newValue: newVal } : void 0
      )
    }
  }
  return r
}

前面我们说过,Ref就像一个容器,里面包裹着原始的数据值(value属性)。而这里的ref函数完成的就是封包的动作,同时它又对value属性进行了劫持。但是,ref函数只是解决了基本数据类型在函数之间传递过程中保持响应式的问题,对于实现诸如对象解构和扩展运算符操作后的数据响应式则无能为力。这时就需要用到toRefs函数。

export function toRefs<T extends object>(
  object: T
): { [K in keyof T]: Ref<T[K]> } {
  // 开发环境提示object应该是响应式对象,即先被reactive()函数处理
  if (__DEV__ && !isReactive(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = {}
  for (const key in object) { // 遍历object的key,并将其value转化为Ref类型
    ret[key] = toProxyRef(object, key)
  }
  return ret
}
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
}

toRefs方法通过遍历一个reactive对象的每一个key,将其属性值转化为Ref类型,这样经过解构之后的属性仍然是可响应的。需要注意的一点是,因为toRefs的使用前提是对象本身是可响应的,所以在toProxyRef函数中对value劫持时无需进行tracktrigger


以上是我对Ref粗浅分析,如理解有误,欢迎指正~