为什么需要ref
我们前两章讲讲解了reactive源码解析和effect源码解析,并且知道了它们是如何实现响应式的,还没看过的小伙伴可以先阅读一下。
我们回顾一下,reactive函数可以创建通过Proxy实现的响应式对象,响应式对象需要在effect中使用才能收集到依赖,在更改响应式对象时,代理会通过trigger通知所有依赖的effect对象,并执行effect的监听方法。
正因为reactive创建的响应式对象是通过Proxy来实现的,所以传入数据不能为基础类型,比如number、string、boolean。
什么是ref
ref对象是对reactive不支持的数据的一个补充,让如基础数据响应式进行支持,以及更方便的对象替换操作推出的。下面我们先了解一下ref的特性。
-
使用
ref或shallowRef函数创建ref对象,ref通过value属性进行访问和修改传入参数。 -
与
reactive不同,ref的参数没有任何限制。 -
使用
reactive可接受的对象为ref参数对象时,isReactive(ref.value)为true。 -
ref在effect监听函数中使用可响应式 -
ref在effect中只有value属性是可响应式的 -
customRef可以创建自定义getter、setter的ref,创建时需要提供一个创建get, set工厂方法,工厂方法会传入收集方法和触发方法,由用户主动触发。如jslet value = 1 const custom = customRef((track, trigger) => ({ get() { track() return value }, set(newValue: number) { value = newValue trigger() } })) -
使用
toRef可以通过proxy的某个属性生成为可以有默认值的ref对象 -
使用
toRefs可以通过proxy的数据结构以及所有属性,生成与proxy数据结构一致的,所有属性值为ref对象的对象
综合上面的特性和之前讲解effect的实现原理,能猜得到ref对象会对value属性的修改和获取时进行拦截,在value被get的时候收集依赖,在set的时候获取依赖关联的effect再触发依赖函数。ref对属性修改和获取时不能通过proxy来实现,ref支持基础类型而proxy不支持。收集依赖时不能使用effect文件中的targetMap关联effect,targetMap是WeakMap类型,WeakMap类型仅支持对象作为key,不支持基础类型。
ref和shallowRef的具体实现
接下来我们看看ref和shallowRef的具体实现:
js
// 是否是ref根据属性的__v_isRef决定
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}
export function ref(value?: unknown) {
return createRef(value, false)
}
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
// 创建ref对象,传入raw和是否是shallow
function createRef(rawValue: unknown, shallow: boolean) {
// 如果之前时ref则直接返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
与reactive一样ref创建的响应式对象也分为是否是shallow,ref对象支持对value深度响应式,也就是说ref.value.a.b中的修改都能被拦截,shallowRef对象只支持对value值的响应式。
ref和shallowRef函数都使用createRef来创建ref对象,只是参数的区别。创建的ref对象会附加__v_isRef属性来标识是否是ref对象。在创建ref对象之前会检查入参是否是ref如果是就直接返回入参参数。
我们看到ref函数创建的真实对象是RefImpl,采用了class写法,将raw和shallow作为构造函数,下面我们看看这个class的实现:
js
// Ref对象类
class RefImpl<T> {
// 存放 reactive(raw) 后的proxy
private _value: T
// 存放 raw
private _rawValue: T
// 建立与effect的关系
public dep?: Dep = undefined
// 是否ref的标识
public readonly __v_isRef = true
// 构造,传入raw 和 shallow
constructor(value: T, public readonly _shallow: boolean) {
// 存储 raw
this._rawValue = _shallow ? value : toRaw(value)
// 如果是不是shallow则 存储 reactive proxy 否则存储传入参数
this._value = _shallow ? value : toReactive(value)
}
// getter value拦截器
get value() {
// track Ref 收集依赖
trackRefValue(this)
return this._value
}
// setter value拦截器
set value(newVal) {
// 如果是需要深度响应的则获取 入参的raw
newVal = this._shallow ? newVal : toRaw(newVal)
// 查看要设置值是否与当前值是否修改
if (hasChanged(newVal, this._rawValue)) {
// 存储新的 raw
this._rawValue = newVal
// 更新value 如果是深入创建的还需要转化为reactive代理
this._value = this._shallow ? newVal : toReactive(newVal)
// 触发value,更新关联的effect
triggerRefValue(this, newVal)
}
}
}
如果不是shallow传入的value会通过toReactive转化为reactive,然后存在ref._value中。在get的时候直接返回这个reactive,这就是使用reactive可接受的对象为ref参数对象时,isReactive(ref.value)为true的原因,也是为什么能深度响应的原因。
ref还会存储入参和set的原始值,如果不是shallow则通过toRaw获取,存储在_rawValue属性中,存储这个值是为了能正确的判断值是否被修改。所以下方这种情况是不会调用triggerRefValue的,因为原始值是一样的。
js
const target = { name: 'bill' }
const reTarget = reactive(target)
const targetRef = ref(reTarget)
targetRef.value = target
ref对象还有个非常重要的属性dep,reactive对象是通过targetMap与Dep关联的。reactive收集时通过track函数获取dep,然后通过dep对象调用trackEffects函数来将effect与Dep关联。
reactive触发时通过trigger函数整理相关联的多个dep最终合并成一个dep,然后通过dep调用triggerEffects获取关联的effect收集函数并触发。
dep中的具体细节管理是通过trackEffects函数和effect对象管理的,将dep与effect是由trackEffects函数处理的, 触发是由triggerEffects函数执行的。
也就是说基于现有effect的基础上,创建响应式对象只需要收集时获取dep并调用trackEffects(dep), 触发时获取收集时的dep并调用triggerEffects(dep)。 dep属性就是ref能成为响应式对象的根本原因。
接下来我们看看ref是如何实现trackEffects(dep)和triggerEffects(dep)的。ref在get value时会调用trackRefValue,在set value时,如果value值发生了更改则调用triggerRefValue。可以猜到这两个方法就是实现响应式的关键,接下来我们看看他们的具体实现
js
// 收集 ref 依赖 调用trackEffects(dep)
export function trackRefValue(ref: RefBase<any>) {
// 如果当前开启了跟踪
if (isTracking()) {
// 获取raw ref数据
ref = toRaw(ref)
// 如果当前ref还未初始化dep则创建
if (!ref.dep) {
ref.dep = createDep()
}
// 如果是开发环境,则传入track细节,
if (__DEV__) {
trackEffects(ref.dep, {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep)
}
}
}
// 触发 ref 调用trackEffects(dep)
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
// 获取raw ref数据
ref = toRaw(ref)
// 如果当前ref 有关联的dep
if (ref.dep) {
// 如果当前是开发环境则发送具体触发细节
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
// SET引起的变化
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
trackRefValue会在适当的时候初始化dep并调用trackEffects,triggerRefValue会获取ref的dep并调用triggerEffects,就是我们上面说的内容。
大家注意到传入的ref会调用toRaw方法来重新赋值,这个方法是获取reactive的原始数据的。因为用户可能使用reactive(ref(raw))来获取数据,如果直接使用可能会收集到dep属性的依赖。另外大家思考一下下面这段代码的effect监听函数会触发几次?
js
const countRef = ref(0)
const reCount = reactive(countRef)
effect(() => {
console.log(reCount.value)
})
reCount.value = 3
答案是四次,第一次是首次收集依赖,reactive会收到value的获取,存储value属性的dep附加到targetMap。然后调用ref.value,ref在获取value时会调用trackRefValue,创建dep附加到自身属性上。注意ref.value返回this._value,这时候reactive收到_value属性的获取,存储_value属性的dep,附加到targetMap中。所以创建了三个dep。当发生更改新值存储到ref._value中,而对于reactive来说value和_value是完全没关联的所以会触发两次,而ref自身会触发一次没所以一共是四次。
customRef
接下来我们看看自定义ref方法customRef是如何实现的:
js
// 自定义ref对象类
class CustomRefImpl<T> {
// 依赖dep 存储effets
public dep?: Dep = undefined
// 缓存getter setter
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
// mark ref
public readonly __v_isRef = true
// 传入ref工厂函数
constructor(factory: CustomRefFactory<T>) {
// 构建getter setter,传入track trigger函数
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)
}
}
// 创建自定义ref
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
return new CustomRefImpl(factory) as any
}
customRef会创建CustomRefImpl的一个实例并返回,CustomRefImpl的实现和Ref差不多,使用trackRefValue和triggerRefValue将dep与effect关联实现响应式。不过CustomRefImpl会在工厂函数中传入trackRefValue和triggerRefValue,将收集依赖和触发执行权交给用户。让用户在适当的时候调用。在使用value时候调用生产的get方法,在设置value是调用生产的set方法。一般是在get的时候调用收集函数,set的时候触发函数。
toRefs和ObjectRefImpl
我们在使用reactive时通过缓存属性值很可能会失去响应式特性。因为属性值可能是reactive不支持深入响应的值,这时候缓存属性值,或者是通过ES6解构出来的值是不具备响应特性的。比如在下面这两种使用方式:
js
const reuser = reactive({ name: 'bill', sex: '男' })
const { name } = reuser
const sex = reuser.sex
这样的话就得一直使用reuser.name的方式来进行访问,vue有两个api能很好的解决这个问题,就是toRef和toRefs。
toRef通过reactive代理和代理的某个属性生成为ref并且可以携带默认值。而toRefs根据reactive代理生成所有属性值为ref的对象。生成的ref的value是代理属性值的映射,两端更改都会实时同步,我们看看是如何使用的:
js
const reuser = reactive({ name: 'bill', sex: '男' })
const name = toRef(reuser, 'name', '未命名')
const { sex } = toRefs(reuser)
console.log(reuser.name, reuser.sex) //bill 男
console.log(name.value, sex.value) //bill 男
name.value = 'lzb'
sex.value = '女'
console.log(reuser.name, reuser.sex) //lzb 女
console.log(name.value, sex.value) //lzb 女
delete reuser.name
console.log(reuser.name, reuser.sex) //undefined 女
console.log(name.value, sex.value) //未命名 女
接下来我们看看toRef和toRefs的具体实现:
js
// 将proxy对象和目标的属性 转化为ref 并拥有默认值
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
// 记录代理
private readonly _object: T,
// 要辅助的key
private readonly _key: K,
// 默认值
private readonly _defaultValue?: T[K]
) {}
// 获取value
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
}
}
// 将proxy对象和目标的属性 转化为ref 并拥有默认值
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)
}
// 将proxy对象所有属性转化为ref值
export function toRefs<T extends object>(object: T): ToRefs<T> {
// 只有内部代理才能toRefs
if (__DEV__ && !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
// 分别对所有属性toRef
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
toRef会先查看proxy[key]是否是ref如果是的话直接返回,如果不是则创建ObjectRefImpl并且将参数传入,ObjectRefImpl会标识当前对象是ref类型 (通过__v_isRef属性) ,并且缓存proxy、key和默认值。get value时直接通过proxy[key]来获取并返回,如果回去的值是undefined则使用默认值。set value时则通过proxy[key] = newVal来设置。
toRefs则是将每个属性都调用一次没有默认值的toRef,并且返回与proxy一致的数据结构。
为什么这里的ObjectRefImpl类不需要dep属性和收集依赖和触发更改呢?这是因为_object属性本身是proxy类型,当我们在使用proxy[key]就实现了收集依赖,在proxy[key] = newVal是就触发了更改。
其他辅助方法
ref文件中还声明了其他辅助方法,比如triggerRef手动触发ref的更改使关联的effect重新执行收集函数;unref获取ref的原始值。这两个方法比较简单直接看源码即可,这里就不再讲解了。
js
// 手动触发 ref
export function triggerRef(ref: Ref) {
// 开发环境用当前值做最新值变化
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}
// 解构ref,直接返回value
export function unref<T>(ref: T | Ref<T>): T {
return isRef(ref) ? (ref.value as any) : ref
}
还有一个辅助方法proxyRefs,这个方法将一个对象直属属性内的所有ref属性值解构访问 (不需要通过value下标访问) 。什么是直属属性就是第一层属性,比如下方的代码:
js
const name = ref('bill')
const unUser = proxyRefs({
name: name,
adderss: {
city: ref('珠海')
}
})
console.log(unUser.name) // bill
console.log(unUser.address.city) // Ref
unUser.name = 'lzb'
console.log(unUser.name) // lzb
proxyRefs只对第一层属性的ref解构。我们看看它的源码:
js
// 浅解构ref处理器
const shallowUnwrapHandlers: ProxyHandler<any> = {
// getter将unref方便访问
get: (target, key, receiver) => {
return unref(Reflect.get(target, key, receiver))
},
// setter先查看是否是ref,如果是则更新value
set: (target, key, value, receiver) => {
const oldValue = target[key]
// 如果新数据不是ref但旧数据是则更新value
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
}
// 创建代理ref 解构方便使用
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
// 如果是reactive对象则无需解构
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
当proxyRefs入参是reactive对象时则直接返回,reactive对象本身会对ref解构,而且是深度的,这里就不需要处理。有个特殊情况shallowReactive对象不会对ref解构,但是也直接返回了,也就是说这个方法对shallowReactive对象时无效的!
如果proxyRefs入参不是reactive对象,则创建代理,get拦截器通过unref来获取值返回,set拦截器通过判断当前要更新的是否是ref如果是则更新value。
到这里我们ref的所有内容就已经讲完了,接下来日常小结。
小结
ref对象自身附加了dep,在收集依赖时通过trackEffects函数,触发时通过triggerEffects函数ref能够创建深度响应式是依赖了reactiveProxy代理对象可以通过toRef和toRefs辅助方法保持对单个属性的引用,赋值修改会映射到ProxycustomRef函数可以创建自由度极高的响应式对象