Vue3.0源码解析「reactive」篇 — 3.ref functions

1,973 阅读6分钟

Ref 类型定义

Ref 接口定义了 ref 函数返回的类型签名,value 属性保存着 ref 的原始值,[RefSymbol] 是内部定义的唯一符号用于类型区分,_shallow 标识标志这个这个 ref 是否为一个浅层 ref

declare const RefSymbol: unique symbol

export interface Ref<T = any> {
  value: T
  /**
   * Type differentiator only.
   * We need this to be in public d.ts but don't want it to show up in IDE
   * autocomplete, so we use a private Symbol instead.
   */
  [RefSymbol]: true
  /**
   * @internal
   */
  _shallow?: boolean
}

UnwrapRef

UnwrapRef 是用于解套 Ref 时的类型声明,通过 infer 推断出包裹类型再利用 UnwrapRefSimple 进一步解套。UnwrapRefSimple 定义的规则如下:

  • Function类型、集合类型、基本类型、Ref类型、RefUnwrapBailTypes 直接返回,因为不涉及嵌套解套;
  • 数组和对象,有 ref 转换的时候会对对象进行 reactive所以要进行深层的解套。
export type UnwrapRef<T> = T extends Ref<infer V>
  ? UnwrapRefSimple<V>
  : UnwrapRefSimple<T>

type UnwrapRefSimple<T> = T extends
  | Function					// a function 
  | CollectionTypes		// Map|Set|WeakSet|WeakMap
  | BaseTypes					// string | boolean | number
  | Ref								// a Ref
  | RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
  ? T
  : T extends Array<any>
    ? { [K in keyof T]: UnwrapRefSimple<T[K]> }
    : T extends object 
       ? UnwrappedObject<T> 
       : T

由于对对象结构时 in keyof 会导致 symbol 作为签名的属性丢失,导致一个对象失去 Object 类型的一些基本约束,所以要手动访问并且添加(这些属性签名本来就是硬编码到 Object 类型的):

type UnwrappedObject<T> = { [P in keyof T]: UnwrapRef<T[P]> } & SymbolExtract<T>
// Extract all known symbols from an object
// when unwrapping Object the symbols are not `in keyof`, this should cover all the
// known symbols
type SymbolExtract<T> = 
	(T extends { [Symbol.asyncIterator]: infer V }? { [Symbol.asyncIterator]: V } : {}) &
  (T extends { [Symbol.hasInstance]: infer V } ? { [Symbol.hasInstance]: V } : {}) &
  (T extends { [Symbol.isConcatSpreadable]: infer V } ? { [Symbol.isConcatSpreadable]: V } : {}) &
  (T extends { [Symbol.iterator]: infer V } ? { [Symbol.iterator]: V } : {}) &
  (T extends { [Symbol.match]: infer V } ? { [Symbol.match]: V } : {}) &
  (T extends { [Symbol.matchAll]: infer V } ? { [Symbol.matchAll]: V } : {}) &
  (T extends { [Symbol.replace]: infer V } ? { [Symbol.replace]: V } : {}) &
  (T extends { [Symbol.search]: infer V } ? { [Symbol.search]: V } : {}) &
  (T extends { [Symbol.species]: infer V } ? { [Symbol.species]: V } : {}) &
  (T extends { [Symbol.split]: infer V } ? { [Symbol.split]: V } : {}) &
  (T extends { [Symbol.toPrimitive]: infer V } ? { [Symbol.toPrimitive]: V }: {}) &
  (T extends { [Symbol.toStringTag]: infer V } ? { [Symbol.toStringTag]: V } : {}) &
  (T extends { [Symbol.unscopables]: infer V } ? { [Symbol.unscopables]: V } : {})

ToRef

主要是给包含 Ref 的 Union 类型使用的,我们希望 union 类型不产生类型分发:

export type ToRef<T> = [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
export type ToRefs<T = any> = {
  // #2687: somehow using ToRef<T[K]> here turns the resulting type into
  // a union of multiple Ref<*> types instead of a single Ref<* | *> type.
  [K in keyof T]: T[K] extends Ref ? T[K] : Ref<UnwrapRef<T[K]>>
}

ref

来看看 ref 函数的实现,前三行都是函数重载,除了不传参数的时候会直接返回一个 Ref<T|undefined>,否则 ref 返回的都是 Ref<UnwrapRef<T>>先解包再封包的 Ref 类型,其中 ToRef 组织了条件分发,上面有单独说明:

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) {
  return createRef(value)
}

createRef

ref 函数内部调用了 createRef,通过一个 rawValue 构造 ref 类型,如果rawValue已经是 ref 类型那直接返回,不然则通过 RefImpl 构造一个 Ref 类型:

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

RefTmpl

RefImpl 就是实际的 Ref 类型构造器,包含两个私有属性:

  • _value:存储传入的 rawValue
  • __v_isRefRef 标识符,用于判断;

然后是构造函数,接收两个参数 _rawvalue 就是原始值,_shallow 定义是否为浅层 ref,如果不是浅层 ref 会在传入的参数为 object 的时候将其转化为 reactive

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  // ...

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

接下来就是 ref 响应式的主体实现了,回顾一下 reactive 构造响应式对象,是通过 proxy 给对象的所有属性添加get&set拦截器,并且在拦截器里做 track|trigger 跟踪 effect

Ref 对象只有一个响应式属性就是 value,所以 RefImpl 里直接对这属性添加 get&set 绑定查询设置函数:

  • get:调用 track 函数收集副作用,本身作为 target 传入(this);
  • set:先通过 hasChanged (其实就是===额外加了NaN的判断逻辑) 判断 rawValue 是否有变,有变化就更新 _value 的值,更新规则和 constructor 一致。最后还会 trigger 触发所有副作用函数。
class RefImpl<T> {
  // ...
  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}


// compare whether a value has changed, accounting for NaN.
export const hasChanged = (value: any, oldValue: any): boolean =>
  value !== oldValue && (value === value || oldValue === oldValue)

ref.spec 里分析对 ref 的测试用例,之前之所以对传入的对象进行 convert 就是为让嵌套的属性可以响应:

it('should make nested properties reactive', () => {
  const a = ref({
    count: 1
  })
  let dummy
  effect(() => {
    dummy = a.value.count
  })
  expect(dummy).toBe(1)
  a.value.count = 2
  expect(dummy).toBe(2)
})

customRef

customRef 用于自定义一个 ref,可以显式地控制依赖追踪和触发响应,接受一个工厂函数,两个参数分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个一个带有 getset 属性的对象

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

export type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T
  set: (value: T) => void
}

customRef通过CustomRefImpl,没啥好说的就把 tracktrigger 注入进去了:

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

  public readonly __v_isRef = true

  constructor(factory: CustomRefFactory<T>) {
    const { get, set } = factory(
      () => track(this, TrackOpTypes.GET, 'value'),
      () => trigger(this, TriggerOpTypes.SET, 'value')
    )
    this._get = get
    this._set = set
  }

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

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

shallowRef

shallowRef 阻止其将内部的元素转换为 reactive

export function shallowRef<T extends object>(
  value: T
): T extends Ref ? T : Ref<T>
export function shallowRef<T>(value: T): Ref<T>
export function shallowRef<T = any>(): Ref<T | undefined>
export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

isRef

isRef 判断参数是否为 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)
}

toRef

toRef 可以用来为一个对象的属性创建一个 ref。这个 ref 可以被传递并且能够保持响应性:

const state = reactive({
  foo: 1,
  bar: 2,
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

函数定义如下,内部调用了 ObjectRefImpl

export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): ToRef<T[K]> {
  return isRef(object[key])
    ? object[key]
    : (new ObjectRefImpl(object, key) as any)
}

ObjectRefImpl 实现的方式其实很简单,这个结构缓存传入对象的引用和键,然后给自己的 value 做个代理到源对象,不管是修改这个 ref还是源对象,修改的都是相同的引用。

如果原对象是个 reactive 对象,set/get会触发其内部的代理所以可以保持响应性。

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(private readonly _object: T, private readonly _key: K) {}

  get value() {
    return this._object[this._key]
  }

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

toRefs

把一个响应式对象/普通对像转换成值为 ref 的普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应。

const state = reactive({
  foo: 1,
  bar: 2,
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型如下:

{
  foo: Ref<number>,
  bar: Ref<number>
}

先构造新对象或数组,内部调用了 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
}

unref

一个很简单的语法糖:

export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
  return isRef(ref) ? (ref.value as any) : ref
}