解读 vue3 的 ref、reactive api

1,283 阅读4分钟

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

之前浏览vue官网镜像站点,知道 vue3 比起 vue2 多了个组合式 api 的特性,但还没时间深入了解它们的源码实现,今天就来研究研究其中的ref、reactive两个组合式api中的响应式api。

此次本文结构如下:

  • ref、reactive 使用方法
  • ref 原理
  • reactive 原理
  • 源码体现的编程技巧

ref、reactive 使用方法

建议看下vue官网镜像站点对ref、reactive的介绍,这里只做简单介绍。

ref 和 reactive 函数都可以返回一个响应式对象,它们用在组件的 setup 选项上。

对于ref,使用如下

const myRef = ref(0)
console.log(myRef.value) // 0

定义了ref对象,需要访问原始值就要通过 myRef.value访问

对于 reactive,使用如下

const myReactive = reactive({a:1})
console.log(myReactive.a)// 1

reactive 函数接受一个对象参数,对于对象深层嵌套的ref对象,会自动进行解包,通过reactive对象访问其中的ref对象时,无需使用.value

const myRef = ref(0)
const myReactive = reactive({a: myRef})
console.log(myRef.value === myReactive.a) // true

Ref 原理

我们为什么要用 ref函数,而不是直接用一个字面量对象 {value: 0}代替 ref(0)呢?

因为ref函数返回的对象很特别,可以在你访问它的值的时候跟踪依赖,修改它的值的时候触发依赖,实现数据的响应式更新。

此时来看一下 vue 源码,ref函数返回值的定义如下:

class RefImpl<T> {
  private _value: T
  private _rawValue: T

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

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

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

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

也就是说在我们访问 refObject.value 的时候会执行一个名为 trackRefValue 的函数,它会维护一个 Dep 集合,将当前实例的 activeEffect加到 Dep 集,activeEffect也会添加这个Dep集

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// ...
  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
  }
}

而在我们修改 refObject.value 的值使其与旧值不同的时候会执行 triggerRefValue(this, newVal)从而执行该该ref对象的 Dep 集上的所有 effect 函数(由用户定义的)。 在上文中, activeEffect是什么东西,有兴趣的同学可以阅读 vue3 源码深入研究,这里就不展开了。

reactive 原理

说白了,还是返回一个入参target的对应的 proxy 对象,在设计上用 WeakMap 缓存了原Target的proxy对象,意味着对同一个对象调用 reactive函数,返回的响应式对象都是同一个代理对象。

这个代理对象也是代理了访问对象属性和修改对象属性的方法,使其在被访问时跟踪收集依赖项,在其被修改时触发 effect 的执行,实现了响应式更新。

既然都用上了Proxy对象,就意味着更多可能。vue3 不仅可以实现数组、普通对象的响应式更新,还支持Map、WeakMap、Set、WeakSet等集合对象的响应式更新。

源码体现的编程技巧

神器的 infer 推断

对于vue代码中的一下 ts 类型声明,你要怎么理解 ShallowUnwrapRef<T>

declare const RefSymbol: unique symbol

export type ShallowUnwrapRef<T extends object> = {
 [K in keyof T]: T[K] extends Ref<infer V>
   ? V
   : // if `V` is `unknown` that means it does not extend `Ref` and is undefined
   T[K] extends Ref<infer V> | undefined
   ? unknown extends V
     ? undefined
     : V | undefined
   : T[K]
}

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
}

这个类型声明十分复杂,分析如下:

  1. 首先要求 T 是一个对象,如果 T[K] 的值满足 Ref 接口,即 T[k] 的实际参数是一个至少含有 value 字段的对象,那么ShallowUnwrapRef<T>对象的 key 为 K 的值的类型就是T[K].value的类型;
  2. 如果推断出 T[K] 的值是可选的Ref接口(即可能是undefined,也可能是Ref接口),则看能否推断出 T[K].value的类型,如果不能,则ShallowUnwrapRef<T>的 key 为 K 的值的类型推断为 undefined,否则推断为T.value的类型或undefined
  3. 如果完全无法推断出 T[K]满足 Ref接口,则ShallowUnwrapRef<T>的 key 为 K 的值的类型推断为 T[K]

看完分析,你是否联想到reactive对象对ref值的解包了?事实上,这个ShallowUnwrapRef<T>是用在一个 proxyRefs 函数上,它的作用是返回一个Proxy对象,代理入参对象的get、set方法,在访问代理对象的key时就会对value进行ref解包——如果是ref对象,就返回value字段的值,否则返回value自身。将proxyRefs 函数返回值声明为ShallowUnwrapRef<T>,其中 T 为 proxyRefs 入参的类型。

const myRef = ref(0)
const myReactive = reactive({ref: myRef})
const a = proxyRefs(myReactive)
a.ref // 在vscode上输入a.ref的时候,不会提示.value的代码补全

这个例子告诉我们,灵活运用类型推断(infer)和类型约束(extends)可以做到很灵活的类型声明,还告诉我们,想要 Proxy 对象做出形如 ref 解包这种奇特的操作,是会大大增加类型声明的复杂度的,这增加了项目的维护成本,在实际项目中还是少用为妙。