上篇博客细聊了reactive对象,这篇博客谈谈对于ref对象的理解
一、ref对象简要介绍
与reactive对象不同的一点是,ref对象不是通过代理实现的。从设计上看,ref对象和reactive对象所针对的数据类型就是不一致的。
- reactive对象是对象类型的包装,使用Proxy实现
- ref对象是number,boolean等基础类型的包装,用getter,setter读写器实现
为什么ref对象不使用proxy实现呢?这儿需要思考为什么ref被设计出来。其实并非不用,实者不能,因为代理只能作用于对象,这些简单的基础类型是不能使用代理的。要去实现这些基础类型操作拦截,只能将这些基础类型进行包装,转换为一个对象来处理。
ref对象分为2类,一个是内置好的ref对象,也是开发中最为常用的响应式对象之一。一个是可以自定义的ref对象,这个对象相较于内置的ref对象而言,有更多的灵活性。下面开始对ref对象进行源码分析。
二、内置ref对象
在开发中,使用ref对象时,都会访问其暴露的value属性,这个属性本质是一个读写器。我们看看ref对象的核心源码:
class RefImpl<T> {
// 使用值
private _value: T
// 原始值
private _rawValue: T
// 依赖
public dep?: Dep = undefined
// 标志,表示是ref对象
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
// 浅响应式时,原始值就是传入值,否则原始值是传入值的原始值。因为传入值可以是代理对象
this._rawValue = __v_isShallow ? value : toRaw(value)
// 浅响应式时,使用值就是传入值,否则转换为响应式对象。
// 当转换一个非对象基础类型时,会返回本身
// ref虽然设计用于非对象基础类型,但也兼容了对象类型的包装
this._value = __v_isShallow ? value : toReactive(value)
}
// getter方法
get value() {
// 收集依赖
trackRefValue(this)
// 返回使用值
return this._value
}
// setter方法
set value(newVal) {
// 是否直接使用传入值,针对一些特殊场景
// 传入值是只读或者浅代理或者这个ref对象本事是浅响应
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
// 直接使用就把传入值当作原始值,否则获取传入值的原始值作为新的原始值
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
// 如果传入值和原始值有变更,则修改原始值和使用值
this._rawValue = newVal
// 使用值在常规场景下需要进行响应式转换
this._value = useDirectValue ? newVal : toReactive(newVal)
// 触发依赖更新
triggerRefValue(this, newVal)
}
}
}
代码其实很简单,本质上就是_value属性用于保存使用值,_rawValue属性用于保存原始值。修改时比较原始值之间是否变更,变更则触发依赖更新,读取时直接获取使用值,触发依赖收集。_value和_rawValue的区别在于_value属性是_rawValue的响应式包装。
如果严格遵循ref的设计,只针对基础普通类型,其实_rawValue属性是可以移除的。就是由于兼容了对象的包装,需要考虑传入的是代理对象等等情况,所以才使用_rawValue用于保存原始数据。
其实传入的如果是一个对象,本质上已经和ref对象关系不大了。参考如下代码:
let data = ref({
a:1
})
let calls = 0
effect(() => {
calls++
console.log(data.value.a)
// 此时进行依赖收集,收集了2次依赖
// 第一次访问value属性,第二次访问a属性
})
data.value.a = 2
// 触发依赖更新,实质是通过reactive对象触发的,而不是通过ref对象触发的
// 修改内层对象的属性值,不会触发setter方法
上述代码中,打印时使用了ref对象,且访问了其包装对象的a属性,这儿进行了2次依赖收集。代码执行流程是这样的,访问了value属性,此时依赖收集,得到的是一个响应式对象,再访问这个响应式对象的a属性,再次进行依赖收集。下面的更改是直接修改的a属性,所以是通过内部的响应式对象来触发的,而不是ref对象触发的。
简单的总结如下:直接操作value属性,那么是通过ref属性进行依赖收集和依赖更新,一旦使用了内层对象属性,本质上是操作的reactive对象。
三、自定义ref对象
自定义ref对象和普通ref对象的设计原理是一致的,区别在于get方法和set方法由开发者自己编写。核心源码如下:
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
// 传入一个工厂方法,这个方法需要返回一个get和set
constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
)
this._get = get
this._set = set
}
get value() {
// 调用get
return this._get()
}
set value(newVal) {
// 调用set
this._set(newVal)
}
}
本质上和ref的设计没有区别,开发者只需要自己实现一个工厂方法。触发依赖收集和触发依赖更新的方法作为入参传入工厂方法。因此什么时候收集依赖,什么时候触发依赖更新都是开发者自己决定的,相对而言十分灵活。示例代码如下:
let value = 1
const custom = customRef((track, trigger) => ({
get() {
// 获取时依赖收集
track()
// 返回数据
return value
},
set(newValue: number) {
// 直接修改数据
value = newValue
// 触发依赖更新
trigger()
}
}))
可以看到,这种自定义的方式,get时的返回和set时的修改,完全由开发者自定逻辑,十分的灵活。因此是否使用,可以结合具体场景考虑。
四、总结
ref对象是响应式系统的基础响应式对象之一。和reactive对象相比,主要是弥补reactive对象无法针对普通类型数据实现响应式的不足,整体设计较为简单清晰,后续博客将继续分析计算属性和异步计算属性。