「这是我参与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
}
这个类型声明十分复杂,分析如下:
- 首先要求 T 是一个对象,如果
T[K]的值满足 Ref 接口,即T[k]的实际参数是一个至少含有 value 字段的对象,那么ShallowUnwrapRef<T>对象的 key 为 K 的值的类型就是T[K].value的类型; - 如果推断出
T[K]的值是可选的Ref接口(即可能是undefined,也可能是Ref接口),则看能否推断出T[K].value的类型,如果不能,则ShallowUnwrapRef<T>的 key 为 K 的值的类型推断为undefined,否则推断为T.value的类型或undefined; - 如果完全无法推断出
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 解包这种奇特的操作,是会大大增加类型声明的复杂度的,这增加了项目的维护成本,在实际项目中还是少用为妙。