Vue3 源码阅读(5)- ref
ref
个人感觉 ref
设计的有些一言难尽,我个人是能用 reactive
的地方绝不用 ref
,使用心智负担有点高…解包的行为有点迷
我们看下 ref
的实现:
1 | export function ref(value?: unknown) { |
发现调用了 createRef
这个工厂方法:
1 | function createRef(rawValue: unknown, shallow: boolean) { |
该方法先判断对象是否已经是 ref
,是则直接返回其本身,否则返回一个 RefImpl
的实例
RefImpl
该类是 ref
的具体实现
成员变量
1 | class RefImpl<T> { |
_value
:在构造时传入的原始对象被处理后的对象_rawValue
:构造时传入的原始对象dep
:依赖该ref
上value
的副作用集合__v_isRef
:ref
类型的标记
constructor
1 | class RefImpl<T> { |
若为浅响应,则不去处理值 value
否则确保 _rawValue
是一个原始对象,并将 _value
设为该值的响应式对象
这里如果传入的是一个普通类型的值(e.g. 字符串、数组),那么这两个成员变量都是值本身
value
getter:
拦截了读取
value
的行为1
2
3
4
5
6
7
8class RefImpl<T> {
...
get value() {
trackRefValue(this);
return this._value;
}
...
}调用
trackRefValue
追踪当前值的变化,接着返回_value
setter:
拦截了修改
value
的行为1
2
3
4
5
6
7
8
9
10
11
12class RefImpl<T> {
...
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);
}
}
...
}如果为浅响应,则直接使用新值,否则确保其是一个原始对象
如果新值和旧值不同,则更新值,与构造器行为一致,并且调用
triggerRefValue
触发值的更新操作
trackRefValue
用于追踪 ref
上 value
的变化
该函数接收一个参数 ref
,即 RefImpl
的实例:
1 | export function trackRefValue(ref: RefBase<any>) { |
跟
effect
中的track
行为一致,需要正在执行副作用且能够追踪:1
2
3if (shouldTrack && activeEffect) {
...
}确保为原始对象:
1
ref = toRaw(ref);
因为可能被
readonly
,reactive
或其它方法处理过调用
trackEffects
:1
2
3
4
5
6
7
8
9if (__DEV__) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value',
});
} else {
trackEffects(ref.dep || (ref.dep = createDep()));
}开发环境下还会额外传 debug 信息,没有影响
triggerRefValue
用于触发追踪 ref
上 value
变化的副作用
该函数接收两个参数,ref
和 newVal
,分别为 RefImpl
的实例和新的值:
1 | export function triggerRefValue(ref: RefBase<any>, newVal?: any) { |
跟上面一样确保为原始对象:
1
ref = toRaw(ref);
调用
triggerEffects
:1
2
3
4
5
6
7
8
9
10if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal,
});
} else {
triggerEffects(ref.dep);
}开发环境下还会额外传 debug 信息,没有影响
customRef
该函数提供了自定义 ref
的能力,如何使用请查阅文档
它返回的是一个 CustomRefImpl
的实例:
1 | export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> { |
CustomRefImpl
该类是 customRef
的具体实现
成员变量
1 | class CustomRefImpl<T> { |
dep
:依赖该 ref 上 value 的副作用集合_get
:拦截读取value
行为的函数_set
:拦截修改value
行为的函数__v_isRef
:ref 类型的标记
constructor
构造函数接收一个工厂函数 factory
1 | class CustomRefImpl<T> { |
该工厂函数被传入了两个参数,分别是 track
方法和 trigger
方法,用户使用两个方法编写追踪、触发响应的逻辑,达到用户自定义行为的目的
工厂函数需要返回 get
,set
方法,这两个方法会分别赋给 _get
,_set
,用户可以在里面利用上面的 track
,trigger
编写读取、修改 value
的逻辑
toRef
用于将某个对象上的某个键值转为 Ref
但是这边有点特殊,它内部的实现并不是跟 RefImpl
,而是一个模拟 Ref
行为的 ObjectRefImpl
1 | export function toRef<T extends object, K extends keyof T>( |
如果键值已经是 ref
,则直接返回,否则用 ObjectRefImpl
构建
ObjectRefImpl
构造器的传参与 toRef
完全一致
直接看下 getter
和 setter
getter
1
2
3
4
5
6
7
8class ObjectRefImpl<T extends object, K extends keyof T> {
...
get value() {
const val = this._object[this._key];
return val === undefined ? (this._defaultValue as T[K]) : val;
}
...
}直接去取目标对象上对应的键值并返回,如果未定义则返回配置的默认值
setter
1
2
3
4
5
6class ObjectRefImpl<T extends object, K extends keyof T> {
...
set value(newVal) {
this._object[this._key] = newVal;
}
}直接修改目标对象上对应的键值
可以看到整个过程内部都没有存储 value
本身,所以只是模拟了 Ref
的行为罢了
因此如果 toRef
处理的不是一个 reactive
的对象,返回的 Ref
并不会是响应式的
1 | const obj = { count: 0 }; |
proxyRefs
我们在编写 SFC
组件时,文档中有提到:
注意,从
setup
返回的 refs 在模板中访问时是被自动浅解包的,因此不应在模板中使用.value
。
该特性就是用这个方法实现的,具体在 packages/runtime-core/src/component.ts
中被调用,在调用完 setup
函数获得 setupResult
后,会将 proxyRefs(setupResult)
处理后再返回
来看下实现:
1 | export function proxyRefs<T extends object>( |
如果目标对象是响应式的,那么不需要处理直接返回,如果不是的话,就返回一个 Proxy
,shallowUnwrapHandlers
监听器中实现了解包的逻辑:
1 | const shallowUnwrapHandlers: ProxyHandler<any> = { |
get
:可以看到返回了一个经过
unref
处理的值,这就是解包set
:如果旧值为
ref
且**新值不为ref
**,将新制赋给旧值的value
其它情况直接赋值就行
PS:一会解包一会不解包其实挺烦的
前面
reactive
和ref
套娃的时候也是一样,只解包对象中的ref
但不解包数组中的,就离谱…心智负担就体现在这了
其它 api
没啥好说的,如果从前面的文章看到这还读不懂剩下的接口,我真的会很无语