Vue3源码06: reactive、ref相关api源码实现

2022-09-27 14:25:06 浏览数 (1)

  • Vue3源码01 : 代码管理策略-monorepo
  • Vue3源码02: 项目构建流程和源码调试方法
  • Vue3源码03: Vue3响应式核心原理
  • Vue3源码04: Vue3响应式系统源码实现1/2
  • Vue3源码05 : Vue3响应式系统源码实现(2/2)

“原本关于响应式源码的分析已经告一段落,因为有了前面几篇文章的基础,阅读剩余部分的源码已经比较容易。但有不少朋友后台留言觉得这部分内容虽然没那么难,但是在日常工作中很常用,加之考虑到内容的完整性,所以就有了这篇文章。笔者在本文将会分析reactive.tsref.ts两个文件中对外暴露的日常经常使用的API对应的源码实现。 ”

reactive.ts

reactive

代码语言:javascript复制
// 代码片段1
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

逻辑很简单,直接调用了createReactiveObject函数:

代码语言:javascript复制
// 代码片段2
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

函数createReactiveObject最关键的逻辑,就是创建了一个Proxy实例并设置处理器。根据类型不同,处理器可能是处理集合类对象的处理器或者处理普通对象的处理器。从函数reactive代码可知,传入的处理器分别是mutableHandlersmutableCollectionHandlers。接下来我们看看这两个处理器中的代码实现。

代码语言:javascript复制
// 代码片段3 mutalbeHandlers相关代码
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()

function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

从代码片段3中可以看出,最重要的逻辑是createGettercreateSetter两个函数的返回值。我们先来看看createGetter的代码实现:

代码语言:javascript复制
// 代码片段4
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array   integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }

我们从代码片段4中可以看出,createGetter函数的内部条件判断很多,这是因为该函数不仅承担了返回别的场景下的get方法。从这里可以看出:

代码语言:javascript复制
// 代码片段5
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

createGetter函数内逻辑零散,需要处理多种边界条件,最核心的代码其实只有一行:

代码语言:javascript复制
// 代码片段6
if (!isReadonly) {
    track(target, TrackOpTypes.GET, key)
}

因为对于const get = /*#__PURE__*/ createGetter()来讲,没有传参数,isReadonly默认为false,会进行依赖收集,关于依赖收集的原理和实现,上一篇文章已经详细讲过此处不再赘述,至于createGetter的原理和createGetter很相似,关键是触发更新,而触发更新的原理和实现,也在上一篇文章进行了详细的介绍,此处也不赘述。

代码语言:javascript复制
// 代码片段7 mutableCollectionHandlers相关代码
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

const [
  mutableInstrumentations,
  readonlyInstrumentations,
  shallowInstrumentations,
  shallowReadonlyInstrumentations
] = /* #__PURE__*/ createInstrumentations()

从代码片段7可以看出关键的地方是mutableInstrumentations,我们进入函数createInstrumentations看看具体实现:

代码语言:javascript复制
// 代码片段8
function createInstrumentations() {
  const mutableInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key)
    },
    //省略许多代码...
  }

  const shallowInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, false, true)
    },
   //省略许多代码...
  }

  const readonlyInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, true)
    },
    //省略许多代码...
  }

  const shallowReadonlyInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, true, true)
    },
    //省略许多代码...
  }

  const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  iteratorMethods.forEach(method => {
    mutableInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      false
    )
    //省略许多代码...
  })

  return [
    mutableInstrumentations,
    readonlyInstrumentations,
    shallowInstrumentations,
    shallowReadonlyInstrumentations
  ]
}

不得不说函数createInstrumentations的代码还是挺工整的,不同场景下相同的属性统一用相同的方法实现,只是传递不同的参数来区分。该函数的职责是返回了四个不同场景下的集合对象的处理器。但是很奇怪,为什么函数createInstrumentationGetter中不直接执行一行代码return Reflect.get(target, key, receiver)来完成相关工作,而要大费周章创建一系列的处理器对象?原因不难理解:

不同的instrumentation对应的属性的值不一样,比如同样是gettargetget方法,和不同instrumentation对应的get具体实现是不一样的,通过这种方式可以有效区分mutablereadonlyshallowshallowReadonly几种不同的场景。这种代码的编写方式在实际工作中是完全可以借鉴的,能在一定程度上提升代码的可读性。

shallowReactive

对于shallowReactive函数而言,最关键的地方在于,shallowReactiveHandlersshallowCollectionHandlers两个处理器。我们来先了解shallowReactive的使用示例:

代码语言:javascript复制
// 代码片段9
const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// mutating state's own properties is reactive
state.foo  

// ...but does not convert nested objects
isReactive(state.nested) // false

// NOT reactive
state.nested.bar  

从示例中可以看出,传入shallowreactive的对象,只能让该对象自己的属性是响应式的,对于其属性的属性是无法做到响应式的。而为什么reactive函数可以深度响应式,为什么shallowReactive又不能让对象深度响应式,答案都在代码片段4中createGetter函数到这几行代码:

代码语言:javascript复制
// 代码片段10
// 此处省略许多代码...
const res = Reflect.get(target, key, receiver)
// 此处省略许多代码...
if (shallow) {
   return res
}
// 此处省略许多代码...
if (isObject(res)) {
  // Convert returned value into a proxy as well. we do the isObject check
  // here to avoid invalid value warning. Also need to lazy access readonly
  // and reactive here to avoid circular dependency.
  return isReadonly ? readonly(res) : reactive(res)
}

从代码片段10可以看出,如果是浅层响应式,则直接返回属性值。如果是深层响应式,则相当于对属性值再处理成新一个新的代码对象。这与Vue2中递归执行Object.defineProperty是有区别的,也是Vue3的一个重要的优点,性能上会有比较大的提升。

readonly

我们先来看看readonly的具体使用:

代码语言:javascript复制
// 代码片段11
const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // works for reactivity tracking
  console.log(copy.count)
})

// mutating original will trigger watchers relying on the copy
original.count  

// mutating the copy will fail and result in a warning
copy.count   // warning!

这是一种不允许修改修改属性值的机制。其实源码的关键也在代码片段4中的createGetter函数中:

代码语言:javascript复制
// 代码片段12
if (!isReadonly) {
  track(target, TrackOpTypes.GET, key)
}

也就是说,如果是readonly状态,则不进行依赖收集,而且这种readonly状态默认是深度的,具体原理,在代码片段10中可以清晰的体现。

shallowReadonly

shallowReadonlyshallowReactive原理非常相似,此处不再赘述。

isReactive

代码语言:javascript复制
// 代码片段13
export function isReactive(value: unknown): boolean {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.RAW])
  }
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

该函数的作用在官方文档中这样被描述:

“Checks if an object is a proxy created by reactive() or shallowReactive(). ”

当我们看isReactive的实现的时候,发现里面先判断的是否是只读代理对象,如果是只读代理对象,则判断该对象的原始值的ReactiveFlags.RAW属性,为什么要这么做呢,关键逻辑在下面的代码中体现:

代码语言:javascript复制
// 代码片段14  代码位置:baseHandler.ts中的createGetter,省略了很多其他逻辑
if (key === ReactiveFlags.IS_REACTIVE) {
  return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
  return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
  return shallow
} else if (
  key === ReactiveFlags.RAW &&
  receiver ===
    (isReadonly
      ? shallow
        ? shallowReadonlyMap
        : readonlyMap
      : shallow
      ? shallowReactiveMap
      : reactiveMap
    ).get(target)
) {
  return target
}

这里涉及闭包、利用Proxy处理器的get方法巧妙的获取属性ReactiveFlags.IS_READONLYkey === ReactiveFlags.IS_REACTIVE的值。如果我们直接判断获取值!!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])readonly状态下会直接返回false,所以代码片段13的逻辑相当于先把readonly的影响剥离开来,进而获取ReactiveFlags.RAW属性的值,实际上相当于被代理的原始对象值。

isReadonly

代码语言:javascript复制
// 代码片段15
export function isReadonly(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}

原理同对代码片段14的分析。

isShallow

代码语言:javascript复制
// 代码片段16
export function isShallow(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW])
}

原理同对代码片段14的分析。

isProxy

代码语言:javascript复制
// 代码片段17
export function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}

该函数的官方文档解释如下:

“Checks if an object is a proxy created by reactive(), readonly(), shallowReactive() or shallowReadonly(). ”

toRaw

代码语言:javascript复制
// 代码片段18
export function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

官方文档描述如下:

toRaw() can return the original object from proxies created by reactive(), readonly(), shallowReactive() or shallowReadonly().This is an escape hatch that can be used to temporarily read without incurring proxy access / tracking overhead or write without triggering changes. It is not recommended to hold a persistent reference to the original object. Use with caution. ”

示例:

代码语言:javascript复制
// 代码片段19
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true

markRaw

代码语言:javascript复制
// 代码片段20
export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}
export const def = (obj: object, key: string | symbol, value: any) => {
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: false,
    value
  })
}

官方文档描述如下:

“Marks an object so that it will never be converted to a proxy. Returns the object itself. 示例: ”

代码语言:javascript复制
// 代码片段21
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// also works when nested inside other reactive objects
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

但是用这个功能需要小心,官方文档这样说:

markRaw() andshallow APIs such as shallowReactive() allow you to selectively opt-out of the default deep reactive/readonly conversion and embed raw, non-proxied objects in your state graph. They can be used for various reasons: ”

  • Some values simply should not be made reactive, for example a complex 3rd party class instance, or a Vue component object.
  • Skipping proxy conversion can provide performance improvements when rendering large lists with immutable data sources.

“They are considered advanced because the raw opt-out is only at the root level, so if you set a nested, non-marked raw object into a reactive object and then access it again, you get the proxied version back. This can lead to identity hazards - i.e. performing an operation that relies on object identity but using both the raw and the proxied version of the same object: ”

代码语言:javascript复制
// 代码片段22
const foo = markRaw({
  nested: {}
})
const bar = reactive({
  // although `foo` is marked as raw, foo.nested is not.
  nested: foo.nested
})
console.log(foo.nested === bar.nested) // false

“Identity hazards are in general rare. However, to properly utilize these APIs while safely avoiding identity hazards requires a solid understanding of how the reactivity system works. ”

官方文档描述了上面一大段,之所以让小心使用,根本原因是因为对象被传入markRaw执行后,调用reactive函数会执行createReactiveObject函数,而在该函数中有这样一段逻辑:

代码语言:javascript复制
// 代码片段23
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
    return target
}


function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

由此可以看出,标记为markRaw后,该对象不会初始化Proxy实例,而是直接返回该对象。但是如果是该对象的属性呢,因为这种标记并非是深度标记,所以会出现代码片段22所描述的问题,其实作者就是担心误解导致用错了才在文档里面反复强调。

toReactive

代码语言:javascript复制
// 代码片段24
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

如果是对象就转化成响应式对象,否则直接返回传入的值。

toReadonly

代码语言:javascript复制
// 代码片段25
export const toReadonly = <T extends unknown>(value: T): T =>
  isObject(value) ? readonly(value as Record<any, any>) : value

如果是对象就调用readonly,否则直接返回传入的值。

ref.ts

trackRefValue

代码语言:javascript复制
// 代码片段26
export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    if (__DEV__) {
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep || (ref.dep = createDep()))
    }
  }
}

内部逻辑很简单,直接调用了上一篇文章分析过的trackEffects函数进行依赖收集。我们需要重点把握的时候什么时候会调用trackRefValue进行依赖收集。一共两个地方:

代码语言:javascript复制
// 代码片段27
class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

也就是说,在访问RefImpl实例的value属性的时候,就会进行依赖收集。而在修改value属性的值的时候就会触发更新。需要注意的是,这里的访问器属性转化成ES5的代码后就是Object.defineProperty的语法形式。所以很多人说Vue3的响应式系统是建立在Proxy之上的这句话是存在问题的,应该说是建立在Object.definePropertyProxy的基础之上的。

“网络上有些对Vue3了解并不够深刻的人,听见我这样说当即反问我难道你不知道为什么要重构响应式系统把'Object.defineProperty'替换成'Proxy'吗?,对方甚至脱口而出Vue是垃圾,透过屏幕感受到了强烈的侮辱,一时间竟不知道说什么好,当即退出了那个所谓的技术交流群。希望我的读者朋友们都是理性的实事求是的,而不是人云亦云,罔顾事实而企图争个你死我活,总有些奇怪的人容易随便找个理由嫉恶如仇般的站在某个不明所以的制高点对一个甚至都不认识的人进行抨击,简直就是民族不幸的根源,一个人人理性,踏实做事,懂得尊重,谦卑善良,好学上进的民族,才可能带领世界走向更好的明天。否则就会有各种战争、灾难,祸满人间。 ”

另一个进行依赖收集和触发更新的场景,是自定义的响应式对象,其类的代码实现如下:

代码语言:javascript复制
// 代码片段28
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>) {
    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)
  }
}

下面会有相关的API介绍,再请大家回到这里阅读。

triggerRefValue

代码语言:javascript复制
// 代码片段29
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

这里也很简单,只是调用了上一篇文章详细介绍过的triggerEffects函数。至于调用地方同依赖收集函数trackRefValue的介绍。

ref

代码语言:javascript复制
// 代码片段30
export function ref<T extends object>(
  value: T
): [T] extends [Ref] ? T : Ref<UnwrapRef<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, false)
}

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

可以看出这里和调用reactive函数的不同,ref返回的是一个RefImpl实例。而reactive返回的是一个Proxy实例。而上文也提到过,RefImpl实例有访问器属性。

shallowRef

ref,仅仅传递给creteRef的参数不同。主要影响在RefImple的构造函数:

代码语言:javascript复制
// 代码片段31
// 此处省略许多代码...
constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
}
// 此处省略许多代码...

会发现如果__v_isShallowtrue则传入的值是什么,就保存什么值,否则就进行相应的处理。

triggerRef

代码语言:javascript复制
// 代码片段32
export function triggerRef(ref: Ref) {
  triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}

请看上文triggerRefValue部分的内容。

unref、isRef

代码语言:javascript复制
// 代码片段33
export function unref<T>(ref: T | Ref<T>): T {
  return isRef(ref) ? (ref.value as any) : ref
}

export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true)
}

逻辑过于简单,不在此赘述。

proxyRefs

代码语言:javascript复制
// 代码片段34
export function proxyRefs<T extends object>(
  objectWithRefs: T
): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

proxyRefs是一个比较重要的函数,给我们带来的最直接的影响是,从前面我们知道要使用ref对应的值,都会有个.value来取值的动作,但是又了这个proxyRefs函数,在template中使用对应的值,就不用再写.value。原因在shallowUnwrapHandlers中有体现:

代码语言:javascript复制
// 代码片段35
const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      return Reflect.set(target, key, value, receiver)
    }
  }
}

会发现这里用到了上文介绍过的unref函数。

customRef

代码语言:javascript复制
// 代码片段36
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
  return new CustomRefImpl(factory) as any
}

这里用到了上文介绍的CustomRefImpl,其实该类的工作就是把依赖收集和触发更新的操作让用户手动来触发。篇幅太长,就不在这里粘贴官方文档的解释了,朋友们可以自行查阅。

toRefs 、 toRef

代码语言:javascript复制
// 代码片段37
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
}

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)
}

可以看出toRefs中对对象的每个属性或者数组的每个元素都执行了toRef操作,而toRef的核心功能是如果isReffalse,则新建ObjectRefImpl实例:

代码语言:javascript复制
// 代码片段38
class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(
    private readonly _object: T,
    private readonly _key: K,
    private readonly _defaultValue?: T[K]
  ) {}

  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
  }
}

发现初始化这个实例很特别,似乎什么都没做,因为从get value()看,里面只是取了传入的对象 对应的属性,这会发生什么呢?实际上,由于传入的对象本身是isReactivetrue的对象,也就是说该对象是响应式的。这样执行toRef后让那些属性本身变成了响应式,而这个响应式能力却是原始对象本身的。这也就是为什么toRefs的返回值可以解构出来属性,但却不会丢失响应式能力。

toRefs在实际开发中用到什么场景呢?按照Vue官方文档的说法:

“toRefs is useful when returning a reactive object from a composable function so that the consuming component can destructure/spread the returned object without losing reactivity: ”

示例:

代码语言:javascript复制
function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  // ...logic operating on state

  // convert to refs when returning
  return toRefs(state)
}

// can destructure without losing reactivity
const { foo, bar } = useFeatureX()

在编写业务代码的时候常常有下面这种写法:

代码语言:javascript复制
import { toRefs } from 'vue'

export default {
  setup(props) {
    // turn `props` into an object of refs, then destructure
    const { title } = toRefs(props)
    // `title` is a ref that tracks `props.title`
    console.log(title.value)

    // OR, turn a single property on `props` into a ref
    const title = toRef(props, 'title')
  }
}

这是因为如果正常解构props会丢失数据的响应式能力,而toRefs返回值的每一个属性值都是响应式的,所以不会丢失。

  • Vue3源码01 : 代码管理策略-monorepo
  • Vue3源码02: 项目构建流程和源码调试方法
  • Vue3源码03: Vue3响应式核心原理
  • Vue3源码04: Vue3响应式系统源码实现1/2
  • Vue3源码05 : Vue3响应式系统源码实现(2/2)

0 人点赞